Memory MCP
Quazzar Cloud OS ships a built-in Model Context Protocol (MCP) server that lets external AI clients — Claude Desktop, Claude Code, ChatGPT (via an MCP bridge), Cursor, VS Code Copilot, Molly, and any other MCP-compatible tool — read and write durable memories backed by the same notes store the Quazzar UI uses.
In a hurry? Jump to the Memory MCP quickstart.
The entire Memory MCP surface is free on every plan. All 12 tools, 4 resources, and 3 prompts — including write, update, delete, and link — work on the Community plan. Semantic search via local Ollama is free. Only cross-node sync (replicating memories between multiple Quazzar nodes) requires Orbit Pro.
Why Memory MCP
- Durable memory across chats. An AI client calls
memory_createonce and every subsequent session can callmemory_searchto recover the content. - Namespaces. Each API key is scoped to a logical bucket (
default,work,personal, …) so Claude Desktop’s memories stay isolated from Molly’s. - Automatic tagging. Every memory the MCP server writes picks up
#mcp,#memory, a client tag (e.g.#claude-desktop), and#ns/<namespace>alongside any user-supplied tags. - Graph. Memories link to each other via
memory_link(or inline[[wikilinks]]) andmemory_neighborsexposes a BFS over the resulting graph. - Scoped credentials. Every key declares which of
read_memory,write_memory,delete_memory,manage_linksit can use; scope denials are enforced at the tool entry.
Endpoints
| URL | Transport | Use for |
|---|---|---|
/mcp | Streamable HTTP (MCP spec 2025-03+) | Modern clients (Claude Desktop, Claude Code, ChatGPT via MCP) |
/mcp/sse + /mcp/messages | Legacy SSE | Older clients that predate streamable HTTP |
/api/mcp/keys | Session-authed REST | Key management from the Settings UI |
/api/mcp/sessions | Session-authed REST | Active-session list and disconnect |
The bearer token (the raw quaz_... string shown once when you create a key) goes in Authorization: Bearer <key> on every MCP request.
Tools
All 12 tools are available on every plan.
memory_search
Full-text + semantic hybrid search across the user’s memories. Uses FTS5/BM25 with a Porter stemmer for precise lexical ranking, fused with semantic similarity via Reciprocal Rank Fusion (RRF) with a query-intent-adaptive balance. Related notes are automatically boosted via the wiki-graph, and queries are expanded with auto-learned synonyms.
{
"query": "last week's incident",
"limit": 20,
"namespace": "work",
"tags": ["urgent"],
"min_importance": 0.3,
"source": "mcp",
"search_mode": "hybrid"
}Returns { memories: MemoryHit[] }. Every hit is slim (id, title, snippet, tags, importance, last_referenced_at, score, score_breakdown). search_mode accepts "hybrid" (default), "lexical", or "semantic". score_breakdown carries { bm25_rank, semantic_rank, rrf, link_boost, alpha, intent, importance } so callers can see exactly how the ranking was composed.
How ranking works (hybrid mode):
- FTS5/BM25 candidates — full-text match with Porter stemming across title (highest weight), tags, and content. Falls back to LIKE substring matching when FTS5 is unavailable.
- Semantic candidates — cosine similarity over locally-computed note embeddings.
- RRF fusion —
score = alpha/(k + bm25_rank) + (1-alpha)/(k + semantic_rank)wherealphais set by a regex query-intent classifier (how-to queries lean lexical; find/locate queries lean semantic). - Wiki-graph link-boost — notes that are linked from high-ranking results receive a score multiplier, surfacing related context automatically.
- Importance multiplier — applied as a small additive factor so highly-starred memories rise slightly without dominating.
- Confidence multiplier — a memory’s confidence (0–1, learned from 👍 / 👎 on Molly replies) scales its score between ×0.9 and ×1.1. A memory nobody has rated stays neutral (0.5 → ×1.0), so confidence reorders near-ties but never buries a strong match. It shows up as
confidenceinscore_breakdown.
Synonym expansion — abbreviations (MRR (Monthly Recurring Revenue)), co-occurring terms, and embedding-similar terms are learned in the background and silently expanded at query time, so “MRR” and “monthly recurring revenue” match each other without any manual tag setup.
memory_get
Fetch a single memory, optionally including wiki-link references.
{ "id": "<uuid>", "include_links": true }Touches last_referenced_at so the memory rises to the top of memory_recent.
memory_create
Persist a new memory. Auto-tags with #mcp, #memory, the client tag, #ns/<namespace>, and any #tags and [[wikilinks]] the Notes parser extracts from the body.
{
"content": "Ran migrations on node-01 at 14:32 UTC.",
"title": "Migration run 2026-04-21",
"tags": ["deploy"],
"importance": 0.7,
"namespace": "work",
"visibility": "workspace"
}visibility is optional (shared, workspace or private; default workspace). The server may raise it for sensitive content but never lowers it. See Visibility tiers.
Every write passes the privacy guard first. A blocked write returns a pii_blocked error and stores nothing. A write held for review returns a normal (non-error) result:
{
"status": "quarantined",
"quarantine_id": "0b8c…",
"pii_class": "financial_iban",
"message": "memory held for the owner's review: it looks like it contains an IBAN bank account number. …"
}memory_update
Patch an existing memory. metadata_patch merges into the existing JSON (it does not replace it). add_tags / remove_tags mutate the tag list without touching the system tags. visibility can only raise the tier (shared < workspace < private); lowering it is an owner decision made in the Notes app. Content, title, tag and metadata changes go through the privacy guard like memory_create; a held update leaves the stored memory unchanged until you release it.
memory_delete
Soft delete (default) tags the memory with #archived and sets importance to 0 so search stops surfacing it; the row stays visible in the Notes UI. Hard delete (soft: false) removes the row and its graph edges.
memory_link / memory_unlink
Create or remove a wiki-link edge between two memories. bidirectional: true writes the reverse edge as well. Both sides are verified to belong to the caller.
memory_list_namespaces
Returns every namespace visible to the caller with memory counts and the most recent updated_at.
memory_list_tags
Every tag with its occurrence count, sorted descending. Optional namespace and min_count arguments.
memory_recent
The most recently referenced memories (last_referenced_at DESC NULLS LAST, updated_at DESC).
memory_neighbors
BFS the graph from a root memory up to depth (1-3). Returns nodes + edges.
memory_stats
Aggregates: { total, by_source, by_namespace, top_tags[10], top_clients[10], avg_importance }. Useful for the AI to introspect what it knows.
Resources
Resources are a second MCP surface — URIs the client can list and read.
| URI | Description |
|---|---|
memory://namespace/{ns} | All memories in the given namespace (first 100) |
memory://note/{id} | Full body + metadata + links for one memory |
memory://tag/{tag} | Memories carrying the given tag (first 100) |
memory://recent | The 20 most recently referenced memories |
All return application/json.
Prompts
| Name | Args | Purpose |
|---|---|---|
recall | topic | Asks the AI to memory_search(topic) and summarise the hits with citations |
distill | memory_ids | Asks the AI to memory_get a list and synthesise a single new memory via memory_create, linked back to every source |
organize | namespace? | Walks the AI through auditing tag usage and proposing a cleaner taxonomy |
Semantic search
Hybrid ranking uses Reciprocal Rank Fusion (RRF) over FTS5/BM25 lexical and cosine semantic candidate lists, with a query-intent-adaptive alpha, wiki-graph link-boost, and synonym expansion. The old linear weighted sum (cosine*0.55 + lexical*0.35 + importance*0.10) is retained as the legacy mode and used for shadow-comparison logging during the initial rollout period.
The new hybrid ranker ships in shadow mode by default (MEMORY_SEARCH_MODE=shadow). In shadow mode the search response is identical to legacy — but the system silently computes both rankings and logs any divergence to search_comparisons for operator review. Set MEMORY_SEARCH_MODE=hybrid_v2 to activate the new ranking. See the admin guide for details.
Embedding backend
Default: local Ollama running nomic-embed-text, 768 dims. One-line setup:
ollama pull nomic-embed-textVerify it is reachable:
curl -s http://127.0.0.1:11434/api/embed \
-d '{"model":"nomic-embed-text","input":"hello"}' \
| jq '.embeddings[0] | length'
# -> 768Quazzar reads the backend URL from OLLAMA_HOST (default http://127.0.0.1:11434). Override the model with MCP_EMBED_MODEL. If Ollama is not reachable at boot the service logs semantic search disabled (no embedding backend) and the hybrid ranker falls back to lexical-only — every tool keeps working, just without the cosine term.
Progress and reindex
Two session-authed HTTP endpoints support the Settings UI:
GET /api/mcp/memory/embed-stats— returns{ queue_size, total, total_with_embedding, backend_name, backend_dim, enabled }.POST /api/mcp/memory/reindex— pushes every note the caller owns onto the embedding queue. Pending items collapse via the queue’sUNIQUE(note_id)constraint.
A change of embedding model needs a reindex so the cosine space stays consistent.
Analytics dashboard
Settings -> MCP & Memory -> Analytics renders:
- Totals (memories, namespaces, embedded / unembedded split)
- Top tags
- Top clients (Claude Desktop, Cursor, ChatGPT, …)
- Embedding coverage over time
Backed by GET /api/mcp/memory/analytics.
Active sessions panel
Settings -> MCP & Memory -> Active sessions lists every live MCP stream with:
- Client name and IP
- Connection start time
- Transport (streamable HTTP / SSE)
- Disconnect button
Each MCP connection is a separate session even when it comes from the same client. Claude Desktop opens a new stream every restart, so “two rows for Claude” usually means a recent restart left the old row around until its idle timeout.
Backed by GET /api/mcp/sessions and DELETE /api/mcp/sessions/:id.
Namespace rules
- Every API key declares a default namespace.
memory_createaccepts an explicitnamespaceargument that overrides the default for that single call.memory_search,memory_recent,memory_list_tags, andmemory_statsall accept an optionalnamespacefilter.- A memory with an empty
mcp_namespaceappears asdefaultinmemory_list_namespaces.
Auto-tagging
Every memory_create / memory_update merges three tag lists, preserving order and de-duplicating case-insensitively:
- User-supplied tags (
tags[]argument) — come first. - System tags —
mcp,memory,<client>(kebab-case),ns/<namespace>. - Inline tags —
#tagsextracted from the body by the Notes parser.
The result is stored both in the notes.tags CSV column (for the UI) and in the note_tags graph table (for fast count queries).
Scopes and errors
| Tool | Required scope |
|---|---|
memory_create, memory_update | write_memory |
memory_delete | delete_memory |
memory_link, memory_unlink | manage_links |
| Everything else | read_memory |
A scope denial returns:
{"error":"scope_required","data":{"scope":"write_memory"}}Other auth errors: missing_bearer, invalid_token, expired, revoked. Tenant isolation is enforced by the WHERE user_id = ? filter on every tool call — cross-tenant access returns not_found, never 403, so the existence of another user’s memory is not leaked. The same applies to your own memories outside the caller’s visibility tier.
A write rejected by the privacy guard:
{"error":"pii_blocked","pii_class":"financial_card","message":"memory not stored: it contains a payment card number. Remove that detail and save again."}Visibility tiers
Every memory has a visibility tier that decides where it may be shown. Memories always belong to one user; the tier controls which of that user’s surfaces can read it.
| Tier | Readable by | Typical content |
|---|---|---|
shared | Everything below, plus Molly replying on a messaging channel (Telegram, Slack, WhatsApp, Discord, email), where other people such as a family group chat may see the answer | Shopping lists, household routines |
workspace (default) | Your AI clients connected with an MCP key (Claude Desktop, Claude Code, ChatGPT, …) and Molly in the web chat | Most memories |
private | Only the Notes app and Molly in the web chat. Never served to MCP clients or channels, never synced to other nodes | Credentials, health notes, personal finances |
How the tier is set:
- Fail-closed. An unknown tier value reads as
private; an unknown value requested by a client is stored asprivate. - Raise only. On create and update a rules classifier looks for live credentials (
password: …, API keys, private keys), health detail and personal finances, plus a#privatetag. It can raise the tier toprivate, never lower it. MCP clients can request or raise a tier but cannot lower one. - You decide. In the Notes app, the Visible to picker under a note’s title changes the tier in either direction.
- Existing memories from before tiers were introduced are
workspace, which is exactly what they could reach before.
Enforcement lives in the Memory service, so Molly and external MCP clients get the same rules on every path: memory_get, memory_update, memory_delete, memory_link and memory_neighbors answer not_found for a memory outside the caller’s tiers; memory_recent, memory_stats, memory_list_namespaces and memory_list_tags count only what the caller may read. memory_search ranks four times the requested number of results, drops what the caller may not read, then trims to the limit, so private memories that rank highly do not crowd out the ones a client may see.
Privacy guard
Before any memory is stored, a privacy guard checks the title, body, tags and text metadata for personal data. It has three layers:
- Checksummed identifiers. Payment card numbers (card brand prefix and Luhn check), IBANs (mod-97), US social security numbers (valid ranges plus an “SSN” keyword), Russian SNILS (checksum plus the keyword or the
123-456-789 01layout) and a Russian personal INN (12-digit checksum plus an “ИНН” or “INN” keyword). The value has to be real: a note that only mentions an “IBAN column” or “ИНН” passes. - Keyword next to a value. A passport, driver licence or date-of-birth keyword followed by a number or date, and the pay of a named person (a name, a pay word and an amount together, unless words like “total” or “average” show it is an aggregate).
- Optional local model. With
QUAZZAR_AI_MEMORY_PII_LLM=on, notes that mention health or HR detail near a person’s name are checked by a model running on this node (Ollama or LocalAI). Content is never sent to a cloud model. If no local model answers, the note is held.
| Outcome | What happens |
|---|---|
| Allow | Stored as a normal memory |
| Quarantine | Held for your review: IBAN, INN, passport, licence, date of birth, a named person’s pay, health / HR detail (layer 3). Not embedded, not searchable, not synced, not shown to any AI |
| Block | Nothing is stored: payment cards, social security numbers, SNILS |
If the guard itself fails, the write is quarantined, never silently allowed. Each block or quarantine is written to an audit log that records the kind of data and which check fired, but never the value.
Reviewing held memories
When something is held, a Quarantine entry appears in the Notes library with a count. It opens a list of the held memories, the kind of data each one contains, and the client that tried to save it:
- Release stores it as a normal memory with the
privatetier. A held update is applied to the memory it targeted. Change the tier with Visible to if your MCP clients should see it. - Discard deletes it for good.
Only you see your held memories. Molly’s /remember shows “Held for your review” when the guard holds a note.
Kill switches
| Variable | Default | Effect |
|---|---|---|
QUAZZAR_AI_MEMORY_PII_GUARD | on | off stores every write without the guard |
QUAZZAR_AI_MEMORY_TIERS | on | off stops tier classification and filtering, so every surface reads every memory as before |
QUAZZAR_AI_MEMORY_PII_LLM | off | on enables layer 3 with a node-local model |
QUAZZAR_AI_MEMORY_PII_LLM_MODEL | (first local model) | Model name for layer 3 |
Cross-node sync (Orbit Pro)
When you run multiple Quazzar nodes under one Control Panel, opt-in per-namespace replication keeps memories consistent across nodes. Settings -> MCP & Memory -> Cross-node sync shows one row per namespace with enable / disable / resync / clear-state controls.
Syncs:
- Content, title, folder, tags (set-union), metadata (per-key LWW), importance (max),
last_referenced_at(max), tombstones on hard-delete. - Only
sharedandworkspacememories.privatememories and quarantined writes never leave the node. When a synced memory becomesprivate, Quazzar sends the other nodes a delete (IDs only, no text), so their copies are removed. It never syncs again while it stays private.
Does not sync:
- Embedding vectors (regenerated locally on arrival), your API keys (node-local), the embed queue.
Conflicts resolve via Hybrid Logical Clocks (physical_ms:counter:node_id). Push is rate-limited on the CP to 120 requests per minute per user; exceeding it returns 429 and the outbox retries with exponential backoff.
See the Cloud OS API reference for the full node-side and CP-side endpoint surface.
Cross-node sync requires Orbit Pro. Single-node Memory MCP is free on every plan.
Automatic deduplication
When a note is saved and its embedding is computed by the background embed worker, the worker scans the same user’s notes in the same namespace and with the same visibility tier for near-identical matches (cosine similarity ≥ 0.92). A private memory is never merged into a less private one. When a duplicate is found:
- The older or more-referenced note is kept as the canonical entry.
- Tags and wiki-links from both notes are merged into the surviving note.
- The richer content body is preserved.
- The duplicate is soft-deleted and its graph edges are re-pointed.
This runs asynchronously — there is a brief window between memory_create and dedup resolution, but the create call itself is never slowed. Dedup errors are logged and both notes are kept intact (fail-safe).
Knowledge self-maintenance
Memories age, go wrong and contradict each other. Cloud OS keeps track of how useful each one is and flags the ones that need a human decision. Nothing is ever deleted automatically.
Confidence
Every memory has a confidence between 0 and 1, starting at 0.5.
- Your feedback. Each final Molly reply has 👍 / 👎 buttons. A 👍 raises the confidence of every memory Molly used for that reply by 0.05; a 👎 lowers it by 0.10 (bad memories sink faster than good ones rise). Clicking the same button twice does nothing; switching from 👍 to 👎 applies only the difference.
- Usage. Cloud OS records which memories were injected into which reply. A memory that helped answer 3 or more different questions within 7 days gets a small +0.01 boost, at most once a day.
Confidence is a mild ranking factor in memory_search (see step 6 above) and drives the low confidence check below.
Daily knowledge healer
Once a day the healer checks every memory written by an AI client or Molly. Notes you wrote yourself in Orbit Notes are never judged. It flags:
| Check | Rule |
|---|---|
| Stale | Older than 90 days, never used in a Molly reply, and not returned by any search for 90 days |
| Contradiction | Two memories in the same namespace about nearly the same thing (embedding similarity ≥ 0.85, or the same title) that say different things |
| Low confidence | Confidence fell below 0.3, usually after several 👎 |
Flagged memories go into a review queue. When a flag no longer applies, the next run closes it on its own: for example, when a stale memory gets used again or when a memory’s confidence recovers.
Knowledge gaps
When you ask Molly a question and her memory search finds nothing relevant, the question is saved as a knowledge gap. Only real questions count: requests such as “can you restart nginx?” or “please …”, reminders (“remind me to …”) and statements (“remember that …”) are never logged. Repeated questions are counted once, with their wording normalised (lower-cased, extra spaces and punctuation removed). IDs and SQL fragments are never logged. Once a later question finds a relevant memory, its gap is closed automatically.
Reviewing: Notes → Knowledge health
Open Notes. The Memory sidebar section has a Knowledge health row, and its number counts the open items and gaps. The panel shows when the last check ran and has a Check now button. Items are grouped as Stale, Contradictions, Low confidence and Gaps:
- Archive: soft-deletes the memory. It is tagged
#archived, its importance drops to 0, and it stays visible in Notes. - Keep: closes the item. For a low-confidence memory this also resets its confidence to at least 0.5.
- Merge into older (contradictions): appends the newer memory’s text to the older one under a Merged from… divider, links the two, and archives the newer one. Archive newer / Archive older and Keep both are also available.
- Answer (gaps): type the answer, and it is saved to Molly’s memory with the question as its title. Dismiss hides a gap for good.
A memory you keep or archive is not flagged again for the same reason.
The daily pass also tidies its own bookkeeping: which memories fed which reply, reply ratings and healer run history are kept for 180 days, so a reply older than that can no longer change confidence.
Turning parts off
Every part has its own emergency switch, set in the node’s environment. All of them are on by default:
| Variable | Disables |
|---|---|
QUAZZAR_AI_MEMORY_CONFIDENCE=off | Confidence changes from feedback and usage, and the ranking factor |
QUAZZAR_AI_MEMORY_USAGE=off | Recording which memories fed which reply (feedback then has nothing to adjust) |
QUAZZAR_AI_MEMORY_HEALER=off | The daily healer and Check now |
QUAZZAR_AI_MEMORY_GAPS=off | Knowledge-gap logging |
Privacy
- Memories never leave your node by default.
privatememories are never served to MCP clients or messaging channels and never synced. See Visibility tiers.- The privacy guard blocks or holds memories with personal identifiers before they are stored or indexed.
- Raw bearer keys are bcrypt-hashed before storage. The
key_prefixcolumn stores the first 13 characters solely so the UI can showquaz_AB12...in the list — it is not a lookup primitive. - If cross-node sync is enabled for a namespace, full note bodies for that namespace flow through the Control Panel on the way to other nodes. Run a private CP if that matters.
- If
OLLAMA_HOSTpoints to a remote host, queries leave your box. Keep it on the local loopback to prevent this.
Rate limits
Per-plan MCP rate limits apply once the Orbit Pro gate is wired in:
| Plan | Limit |
|---|---|
| Community | 30 requests / minute |
| Orbit Pro | 120 requests / minute |
| Business | 600 requests / minute |
| Enterprise | unlimited |
Cross-node sync push is capped at 120 / minute / user on the CP side.
Related pages
- Memory MCP quickstart — 5-minute setup for any client
- Orbit Notes — the UI surface for memories
- Molly Memory — how Molly reads and writes memories
- Orbit Pro — cross-node sync tier
- Cloud OS API — REST API for keys, sessions, analytics