MemAI
Server Configuration
Describes the environment variables required to run the server.
| Name | Required | Description | Default |
|---|---|---|---|
| MEMAI_HOME | No | Directory to store the memai.db file. Defaults to ~/.memai. | |
| MEMAI_ADMIN_PORT | No | Port for the admin dashboard (default: 8765). | |
| MEMAI_EMBED_MODEL | No | Set to a Hugging Face repo id or a local path to use a different embedding model instead of the bundled one. |
Instructions
Guidance the server publishes about itself, which clients place ahead of the tool catalog so the model reads it before choosing anything.
This server publishes no instructions, or was last inspected before Glama recorded them.
Capabilities
Features and capabilities supported by this server
Protocol revision2025-11-25
| Capability | Details |
|---|---|
| tools | {
"listChanged": false
} |
| prompts | {
"listChanged": false
} |
| resources | {
"subscribe": false,
"listChanged": false
} |
| experimental | {} |
Tools
Functions exposed to the LLM to take actions
| Name | Description |
|---|---|
| noteA | Save a general long-term memory (fact, decision, finding). Stored as type='note'. Timeless knowledge -- retrieved by relevance, not recency. Bring it back with recall() (or search(type='note')); pulse() also shows the few most recent ones as warm-up breadcrumbs. title: one line naming what this memory is about, in the words someone would look for it by. It is what a list shows instead of the opening of the body, and it outweighs every other field in search, so a title that repeats the type ("note about the parser") names nothing. At most 120 characters, and a name that needs more than that is summarizing the body instead of naming it. content: ONE fact, and what a reader needs to use it -- what holds, where it holds, what it rules out. Retrieval ranks whole memories, so a body answering four questions comes back for all four and is read for one: write the second subject as its own memory, on its own domain, and connect the two with link_memories(). A [[uid]] typed inside a body is a reference a reader can follow, not an edge -- get_relations() and the graph do not see it until link_memories() creates one. Past a couple of thousand characters, a body is usually several memories written as one. domain: the subject this belongs to, as a path from the outermost scope in ('acme/x100/p200'). File it as deep as the fact is specific -- a note about one routine goes on the routine, and still comes back when someone asks about the module or the product above it. also: other domain paths this belongs to, comma-separated. tags: comma-separated keywords and synonyms. Retrieval is BM25 over content, tags and domain paths, and tags weigh second only to the body, so they are where a memory becomes findable by words its own text never uses -- the identifier, the symbol, the error string, the plain-language phrasing someone will actually type. A memory with none is reachable only by quoting itself. review_after: when this stops being safe to trust unchecked, as a date
('2026-11-01') or a span from today ('90d'). pulse() counts what is
overdue in a scope as source_ref: what the fact came FROM -- a path, a URL, a table name -- so a later pass can check the claim against the thing itself instead of inferring what to check from the wording. |
| checkpointA | Snapshot current working state (intent/established/pursuing/open_questions). A summary of where the work stands, so the next session picks up the right bearing via pulse(). Fields are free-length; still prefer a readable summary here and put timeless detail into note() -- checkpoints are read for bearing, not as an archive. One fact per note(), each cited back here by [[uid]] and linked with link_memories(); pulse() returns the latest checkpoint IN FULL, so every session pays for whatever was parked in these fields. Stored as type='checkpoint'. title: one line naming what this memory is about, in the words someone would look for it by. It is what a list shows instead of the opening of the body, and it outweighs every other field in search, so a title that repeats the type ("note about the parser") names nothing. At most 120 characters, and a name that needs more than that is summarizing the body instead of naming it. also: other domain paths this belongs to, comma-separated -- the cross-cutting subjects beside the one it is filed under. See note().
|
| anti_patternA | Record a mistake/temptation to avoid repeating, and the correct approach. Stored as type='anti_pattern'; open ones for a domain are surfaced by pulse().
ONE pitfall per memory: a second temptation from the same session is its own anti_pattern(), connected with link_memories(). See note() on what a body holds and when it is two memories. title: one line naming what this memory is about, in the words someone would look for it by. It is what a list shows instead of the opening of the body, and it outweighs every other field in search, so a title that repeats the type ("note about the parser") names nothing. At most 120 characters, and a name that needs more than that is summarizing the body instead of naming it.
|
| reasoningA | Record an analysis worth keeping: what was thought, and what it settled. For the PROCESS, not the fact it produced -- note() takes the fact. Stored as type='reasoning'; filter search/list_* with type='reasoning' to get these back. ONE analysis per memory: a second hypothesis tested in the same session is its own reasoning(). See note() on what a body holds and when it is two memories. title: one line naming what this memory is about, in the words someone would look for it by. It is what a list shows instead of the opening of the body, and it outweighs every other field in search, so a title that repeats the type ("note about the parser") names nothing. At most 120 characters, and a name that needs more than that is summarizing the body instead of naming it. hypothesis: what you believed going in, as a claim that could be wrong. reasoning: how you tested it -- what you read, ran or compared. result: what came back. The measurement, not the interpretation. revised_belief: what you believe now, and where it differs from the hypothesis. Say plainly when the hypothesis survived unchanged. next_time: what someone hitting this again should do differently.
|
| handoffA | Leave a note for another agent/session picking up this work. Stored as type='handoff'; open ones for a domain are surfaced by pulse().
title: one line naming what this memory is about, in the words someone would look for it by. It is what a list shows instead of the opening of the body, and it outweighs every other field in search, so a title that repeats the type ("note about the parser") names nothing. At most 120 characters, and a name that needs more than that is summarizing the body instead of naming it.
|
| diagramA | Document what a routine does, start to end, as a graph. Stored as type='diagram'. For a PROCESS, not a fact: note() records what is true, checkpoint() where the work stands, this one how a routine runs. Every step is a separate object that can carry its own explanation and its own links to other memories, which is what makes a diagram the source of truth for its domain instead of one more wall of prose. Keep every nodes: [{"key": "load", "label": "Read the export window", "shape": "step", "note": "optional long explanation"}] edges: [{"from": "load", "to": "check", "label": "optional branch"}] key: stable id the edges refer to; letters, digits, '_' or '-'. shape: start|step|decision|io|end. Exactly one 'start' is required and every node must be reachable from it. Cycles are allowed -- a retry loop is a real flow, not a mistake. also: other domain paths this flow belongs to, comma-separated.
Returns {"uid": ...}, or {"ok": False, "errors": [...]} with nothing written at all. Node positions are computed and stored server-side, so the flow renders identically for every reader -- see get_diagram(). |
| diagram_nodeA | Add, patch or remove one step of a diagram. Only the arguments you pass are touched, so patching a note leaves the label alone; pass note="" to clear one. delete=True removes the step together with its edges and its memory links. The whole-graph rules are relaxed here on purpose: a step may sit unattached until you add its edges, which is what lets a flow be built up across several calls. diagram() enforces them. |
| diagram_edgeA | Wire two steps of a diagram together, relabel that wire, or remove it. label carries the condition on a branch out of a decision node ('yes', 'no', 'on timeout'). Calling again with the same endpoints updates the label instead of adding a second edge between them. |
| diagram_linkB | Attach another memory to one specific step of a diagram. What turns a diagram into an index of its domain: the step states what happens, the linked note/anti_pattern/reasoning states why it is that way. Point at the step the memory actually concerns -- for an edge to the diagram as a whole use link_memories() instead. get_memory() on the linked memory reports the diagrams that reference it, so the connection is visible from both ends. |
| diagram_jumpA | Continue one step of a flow into ANOTHER flow, optionally at one of its steps. Not the same statement as diagram_link: that attaches prose explaining a step, this says the rest of this branch is documented elsewhere. Use it where a routine hands off -- a sub-process, an error path owned by another flow, a variant of the same job. Leave |
| diagram_relayoutA | Recompute a diagram's stored node positions from scratch. Positions live in the store, not in a viewer, so every reader sees the same picture and positions hand-adjusted in the admin dashboard persist. This discards those adjustments and rebuilds the layered arrangement -- the fix for a diagram dragged into a mess. |
| get_diagramA | Read a diagram back: format='svg-interactive' to show it, 'json' to reason about it. Formats: 'svg-interactive' (canvas drawing in a pan/zoom shell -- the one to SHOW), 'svg' (same drawing as a plain file, to attach or link), 'mermaid' (portable, but re-lays out and DISCARDS the arrangement the user made), 'text' (the prose projection), 'json' (the full graph with positions, notes and links -- the only round-trippable one, and the one to reason over). Both SVG formats write the markup to a file and return its path plus a
thin index of the steps; the payload is deliberately too small to draw
from. The returned |
| searchA | Keyword search over memory content+tags+domain: FTS5 BM25. Each result is annotated with match_source ("fts", or "uid" for the row a pasted identifier names) and fts_rank (bm25, lower = better). The search only widens the candidate set -- judge the returned candidates yourself. SPEND TERMS FREELY. Every space-separated term is asked for separately and a row matching more of them ranks higher, so piling on synonyms, the identifier, the routine name and the plain-language phrasing into one query costs one call and finds strictly more. Twenty terms beat ten. Write a sentence if that is what you have -- common words score near zero and cost nothing, so there is nothing to strip. Only active memories by default. Returns {"results": [...], "est_tokens": N}. Content is
snippet-truncated per result -- call get_memory(uid) for the full
record; a result's Two annotations worth acting on. A memory marked confidence='contradicted' sorts behind everything that still holds, but it does come back: knowing a claim was ruled out is worth a slot, and it is what stops it being written again. A diagram ranks like any other memory: it comes back when it matches the query, in the position its score earns. Nothing lifts a type to the top, so a flow in the results is a flow this query actually hit -- and when one does show up it is worth opening first, because it states a whole routine the surrounding notes only annotate. type filters (one writer each): 'note', 'reasoning', 'checkpoint', 'anti_pattern', 'handoff', 'diagram'. Ask for type='diagram' to sweep the documented flows on purpose. To recall note()'d knowledge specifically, recall() is the sugar for search(type='note') -- which also means recall() never surfaces a diagram; use search() for that. domain scopes to a path AND everything under it: domain='acme/x100'
searches the module and each of its routines. Give more of the path to
narrow it. A domain naming only the deep end of a path ('p200') is
resolved to the branches it sits in -- every result carries its real
|
| recallA | Recall long-term knowledge saved with note() (type='note'). The dedicated verb for "bring back what I noted": a BM25 search scoped to type='note', ranked by relevance -- which is what you want for timeless facts/rules/decisions. note() has no recency warm-up hook the way checkpoints have pulse(); this (or search(type='note')) is how notes come back. Returns {"results": [...], "est_tokens": N}. Content is
snippet-truncated -- call get_memory(uid) for the full record; a
result's domain scopes to a path and everything nested under it, and resolves a
bare deep segment the same way search() does. Results carry the same
|
| list_by_domainA | List active memories for a domain and its subdomains, most recent first. Fallback when search misses. domain is a path, matched from the outermost segment in: 'acme/x100' lists the module's own memories plus every routine under it. Pass subtree=False for what is filed at exactly that path and nowhere deeper. A domain that matches no path is retried as a run of segments INSIDE
one, so list_by_domain('p200') still finds the routine once it lives
at 'acme/x100/p200'. The literal reading wins whenever it has rows,
and an ambiguous name (the same code under two modules) covers both
branches rather than picking one -- each row's Returns {"results": [...], "est_tokens": N}. Content is
snippet-truncated per result -- call get_memory(uid) for the full
record; a result's |
| list_recentA | List the most recent active memories, optionally filtered by type/domain. A domain covers its subdomains, and a bare deep segment resolves to the branches holding it (see list_by_domain); subtree=False narrows to that exact path. Returns {"results": [...], "est_tokens": N}. Content is
snippet-truncated per result -- call get_memory(uid) for the full
record; a result's |
| timelineA | What else was being written around one memory, in creation order. For the question search cannot ask: not what mentions this record, but what was being written when it was. Neighbours are picked by time alone, so they come back whether or not they share a word with the anchor -- around a checkpoint, that is the notes and pitfalls of the same stretch of work. One of uid or query is required. uid names the anchor outright; query
searches for it and takes the top hit (the same search search() runs,
scoped by domain/type). Give both and uid wins. The response
reports Returns {"anchored_by": ..., "anchor": {...}, "before": [...],
"after": [...]}: domain and type narrow the NEIGHBOURHOOD, not the anchor: a memory named by uid comes back as named, and the records around it are the ones matching the filters. domain covers a path and everything under it, plus what is cross-listed into it, and resolves a bare deep segment the way the other scoped reads do. Archived records are left out of the neighbourhood, as everywhere else by default. Each record is snippet-truncated with |
| list_projectsA | The projects in this home, and which one every call here reads and writes. A project is one SQLite file with its own memories, domains, relations
and diagrams. The active one is switched in the admin dashboard, and a
running server follows on its next call -- so every write's result names
the project it landed in, and pulse() names the one it read. Each entry
carries |
| list_domainsA | List the domain tree: every path with its counts and latest activity. Warm-up discovery. domain is free text and drifts over time (e.g. 'proj-1042' vs 'proj-1042-cache-warmup'), so this surfaces the paths actually in use instead of leaving you to guess one. Ordered by most recent activity. Per entry:
Casing may be enforced store-wide -- call get_domain_case() to see the active policy before coining a new domain. |
| also_domainA | Cross-list an existing memory into one more domain path. For the membership a memory picks up after the fact: it was filed where
it lives, and later turns out to be part of a subject that cuts across
the tree. Every read scoped to Returns {"uid": ..., "also": [...]} with the whole resulting set. A path the memory's own domain already sits under is dropped as redundant, so the echo is what actually holds. |
| unfile_domainA | Drop one of a memory's cross-listings. Does not touch where it is filed. Matched on the exact path: dropping 'acme' leaves a separate membership in 'acme/x100' alone, because that is a different scope. Returns {"uid": ..., "also": [...]} with what remains. |
| get_domain_caseA | Report the store's domain-casing policy. Returns {"mode": "preserve"|"lower"|"upper"}. 'preserve' stores
domains as written; 'lower'/'upper' coerce every domain to that case
on write (a non-conforming domain is adjusted, not rejected, and the
writer's result carries a |
| set_domain_caseA | Set the store's domain-casing policy. mode: 'preserve' | 'lower' | 'upper'. 'preserve' keeps free-text casing; 'lower'/'upper' coerce every domain written from now on to that case. This only governs new writes -- to bring already-stored domains into line, run the "Normalize domains" action in the admin dashboard (it previews collisions before merging variant spellings). Returns the stored {"mode": ...}. |
| pulseA | Session warm-up: latest checkpoint + open handoffs/anti-patterns + recent notes. Picks the checkpoint by created_at DESC, never by similarity -- a similarity-ranked top-1 can return a stale checkpoint over a same-day one, which is exactly the failure mode this avoids. latest_checkpoint is returned in full (that's the point of pulse), with its relations attached so linked memories are visible without a separate get_relations call. handoffs and anti_patterns are notes left for whoever resumes; recent_notes are the newest note()'d facts, as recency breadcrumbs -- for relevance-ranked recall use recall()/search(). Those three lists are snippet-truncated -- call get_memory(uid) for one in full. latest_checkpoint and those three lists carry diagrams lists the documented flows by title only, never inlined: a whole graph would swamp a warm-up. Read one with get_diagram(uid) when the work actually touches that routine. domain warms up a path and everything under it, so pulse('acme/x100')
is the module-wide brief and pulse('acme/x100/p200') the routine's.
A domain that names only the deep end of a path ('p200') is resolved
to the branches it sits in --
A scope holds what is CROSS-LISTED into it as well as what is filed
there, so warming up an end-to-end flow brings back the routines that
are steps of it wherever they live. |
| get_memoryA | Fetch a single memory's full record, including its edit history and relations. A diagram also comes back with its mermaid source, its per-node links
and its jumps to and from other flows; any other memory comes back
with |
| edit_memoryA | Correct a memory's content or its source reference, keeping the previous version. Corrections are common in append-only memory stores that only support delete, not edit; this preserves the old content instead of losing it. mode='append' adds source_ref points the memory at what its claim came from -- the field
note() takes at write time, and the one a later pass checks the claim
against. It is settable on its own, with no title renames the memory: the one line a list shows it by, and the field weighing most in search, at most 120 characters. Settable on its own, like source_ref. A diagram is renamed through its graph instead -- its title is part of what generates the body, so a rename here would be overwritten by the next structural change. tags REPLACES the tag set, comma-separated: pass the whole set that should survive, not the one being added. Settable on its own, and indexed, so this is how an untagged memory becomes findable by the words its body never uses. An empty string leaves the stored tags alone -- clearing them, like clearing a source_ref, is a dashboard edit. Refuses to rewrite a diagram's content: that is generated from the graph, so a hand-written replacement would be silently overwritten by the next structural change -- edit the flow through diagram_node/diagram_edge. Its source_ref is ordinary metadata and is editable here like any other memory's. |
| link_memoriesA | Create a queryable edge between two memories. relation_type is free text but keep it consistent, e.g. 'supersedes', 'relates_to', 'contradicts', 'links_to'. This is what splitting a body into several memories costs: a [[uid]] written inside prose is a reference a reader follows, and only an edge created here is visible to get_relations() and to the graph. Refuses an unknown uid, a memory related to itself, and an edge that already exists with that same type -- each as {"ok": False, "errors": [...]}, so a typo comes back as something to fix instead of a dangling edge or a raw database error. |
| get_relationsA | List all relations (incoming and outgoing) for a memory. |
| set_confidenceB | Set a memory's confidence: unverified | confirmed | contradicted. |
| forgetA | Archive a memory (soft delete -- content is kept, just excluded from default search/list). A |
| purge_memoryA | PERMANENTLY delete a memory + its edit history + relations. Irreversible. Use forget() instead unless the user explicitly asked to permanently remove data -- forget() is reversible (archived, content kept), this is not. Guardrail: confirm_phrase must exactly equal "DELETE ", typed by the user in their own message. Do not construct this string yourself from an inferred "yes"/"confirm" -- it must come from the user actually stating the uid back. |
| move_to_projectA | Carry memories from the active project into another one, and remove them here.
|
| dedup_scanA | Surface likely-duplicate/contradictory memory pairs. Lexical overlap over near-identical text -- each pair carries its
domain scans a path and everything nested under it, which is usually what you want: near-duplicates collect between a module and its own routines. |
| optimize_scanA | Dump the memory corpus compactly so you can plan a curation pass. Step 1 of the "optimize my memories" workflow: every memory's
curation-relevant fields, the relation edges among them, usage counts,
dedup and domain hints, and per-memory Start with what the store already says is suspect:
The listing is slim so a big store fits one response, and a page ends
early at an internal size budget -- BEFORE PROPOSING ANY CHANGE, CHECK IT AGAINST LIVE FACTS, and record what
you checked in each suggestion's |
| optimize_stageA | Stage a batch of curation suggestions for human review in the dashboard. Step 2 of the "optimize my memories" workflow. NOT applied here: the user reviews and applies or rejects each one in the admin dashboard, which backs up before the first apply and can undo any of them. Each suggestion is {"kind", "target_uid", "payload", "rationale", "verified"}. Kinds: compact/reword {"new_content"}, retag {"tags"}, redomain {"domain"}, crosslist {"also": [...]}, set_confidence {"confidence"}, review {"review_after"} (a date or a span like '180d'; '' clears it), archive {"reason"}, link {"from_uid","to_uid", "relation_type"}, merge {"keep_uid","drop_uid"}, distill {"source_uids","new_type","new_content","title"}. link/merge derive target_uid from the payload and distill creates its target -- omit it for those. Destructive kinds (archive, set_confidence=contradicted, merge,
distill) require a non-empty
|
| optimize_runsA | List optimization runs with their review progress. Read-only companion to optimize_stage: after staging, use this to see whether the user has applied/rejected your suggestions in the admin dashboard. Each run carries total/pending/applied/rejected counts, its note, and the safety-backup path once the first apply happened. Applying/rejecting stays in the dashboard by design -- the agent proposes, the human disposes. |
| optimize_statusA | Inspect one optimization run: every suggestion and its decision. Read-only. Returns the run header plus each suggestion's kind, target_uid, payload, rationale, verified, status (pending/applied/rejected) and decided_at -- so you can tell which proposals landed, follow up on rejected ones, or build on applied ones in a later pass. |
| helpA | Explain the memai tools, read directly from their code docstrings. Without arguments: every tool with its one-line summary, and which of them this process did not load (MEMAI_TOOLS). With command='': that tool's signature and its FULL documentation -- longer than the schema description, because the schema is paid for on every request and this is paid for when someone reads it. |
Prompts
Interactive templates invoked by user choice
| Name | Description |
|---|---|
| warm_up | What memai already knows, as text, for a session about to start. The same brief the SessionStart hook emits (see memai.hook), offered here for hosts that surface prompts as commands. A prompt is invoked by the person, which makes it the one place in the MCP protocol where the store can be READ without the agent having decided to read it. |
Resources
Contextual data attached and managed by the client
| Name | Description |
|---|---|
No resources | |
TDQS
Scored across 38 tools
Tools are largely distinct in purpose, helped by detailed descriptions that clarify overlapping retrieval paths (search vs recall vs list_recent/timeline) and diagram operations (diagram_link vs link_memories, diagram_jump vs diagram_link). A few boundaries remain subtle—e.g., search/recall/list_by_domain/list_recent all retrieve memories with different filters—so an agent may need to reason about scope before selecting.
All tool names use snake_case, giving a consistent surface; prefixes like get_/list_/set_/optimize_/diagram_ group related operations. Some tools are named as memory-type nouns (note, checkpoint, anti_pattern, reasoning, handoff, diagram) rather than verb_noun, but that reflects the domain's entities and remains readable.
38 tools is a lot for an MCP server; while most tools have a distinct role, the count places a heavy context/schema burden on the agent and exceeds the 25+ threshold for 'too many' in this rubric. The scope of the server may justify some breadth, but the set is over-large rather than tightly scoped.
Coverage is very strong: memory CRUD/lifecycle (note, get, edit, forget, purge), multiple memory types, relationships, domains/cross-listing, diagrams, projects, and curation workflows (dedup/optimize) are all represented. No obvious major gaps for the stated memory-management purpose; only niche admin actions (e.g., project switching/creation) are delegated to the dashboard.