Skip to main content
Glama

Server Configuration

Describes the environment variables required to run the server.

NameRequiredDescriptionDefault
MEMAI_HOMENoDirectory to store the memai.db file. Defaults to ~/.memai.
MEMAI_ADMIN_PORTNoPort for the admin dashboard (default: 8765).
MEMAI_EMBED_MODELNoSet 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

CapabilityDetails
tools
{
  "listChanged": false
}
prompts
{
  "listChanged": false
}
resources
{
  "subscribe": false,
  "listChanged": false
}
experimental
{}

Tools

Functions exposed to the LLM to take actions

NameDescription
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. domain is where the memory LIVES -- one path, one parent chain. also is for the subjects that cut ACROSS that tree: the same routine belongs to the module it runs in and to the end-to-end flow it is one step of, and neither of those is the other's ancestor. Every read scoped to any of those paths returns it. A path that domain already sits under is dropped as redundant -- the result echoes what was stored.

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 scope.stale and optimize_scan lists it. Leave it empty for anything that does not go stale -- most facts do not, and a date nobody meant is worse than none.

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().

tags carries the synonyms the body never uses: retrieval is BM25 over content and tags, so a memory with none is reachable only by quoting itself. See note() for what belongs there.

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(). also cross-lists it into further domain paths, review_after dates when to recheck it and source_ref says what it came from -- see note().

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.

tags carries the synonyms the body never uses: retrieval is BM25 over content and tags, so a memory with none is reachable only by quoting itself. See note() for what belongs there.

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.

also, review_after and source_ref behave as in note().

tags carries the synonyms the body never uses: retrieval is BM25 over content and tags, so a memory with none is reachable only by quoting itself. See note() for what belongs there.

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(). also cross-lists it into further domain paths -- see note().

content holds the message and what the next agent needs to act on it. Durable knowledge goes to note() and is cited from here -- see note() on when a body 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.

tags carries the synonyms the body never uses: retrieval is BM25 over content and tags, so a memory with none is reachable only by quoting itself. See note() for what belongs there.

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 label objective -- what happens at that step, nothing more. The reasoning, caveats and history belong in that node's note, where they explain without cluttering the flow.

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. domain is the routine's own place in the tree; also is for the flows that run ACROSS routines -- several of them can be steps of one end-to-end process without any of them being the parent of the others. Cross-list each into that process's path and asking about it returns all of them, instead of hoping one search phrasing reaches every one.

review_after and source_ref behave as in note(), and a flow is exactly the kind of memory they are for: it describes code, and the code moves.

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 peer_node empty to arrive at the target diagram as a whole. Stored once and read from both ends, so the return trip already exists and get_diagram(format='json') reports it on both diagrams. uid and node_key are this diagram's side either way, which is also how a jump is deleted from the receiving end.

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 next_step says what to do with the path, at the point where it matters. help(command='get_diagram') has the rest.

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 est_tokens estimates what that full record costs, and the top-level est_tokens is the sum over the results.

Two annotations worth acting on. succeeded_by means something in the store supersedes this memory: read that one instead. collapsed lists near-identical results folded into this one, so a fact written five times spends one slot -- raise limit if you want the copies.

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 domain, which is where to read what the filter actually covered.

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 est_tokens estimates what that full record costs, and the top-level est_tokens is the sum over the results.

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 succeeded_by / collapsed annotations search() explains.

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 domain says which branch it came from. list_domains() is the way to see the paths first.

Returns {"results": [...], "est_tokens": N}. Content is snippet-truncated per result -- call get_memory(uid) for the full record; a result's est_tokens estimates what that full record costs, and the top-level est_tokens is the sum over the results.

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 est_tokens estimates what that full record costs, and the top-level est_tokens is the sum over the results.

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 anchored_by ('uid' or 'query') and the whole anchor record, so which record the timeline is built around is never a guess.

Returns {"anchored_by": ..., "anchor": {...}, "before": [...], "after": [...]}: before is the before records created immediately before the anchor and after the after records created immediately after it, both oldest first, so before + [anchor] + after reads straight down the clock. Neither list contains the anchor.

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 est_tokens for its full content -- call get_memory(uid) to open one.

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 name, memories (the active rows) and active. Names are matched without regard to case wherever a tool takes one.

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: domain (the full path), parent, depth, count (filed at exactly this path), subtree (that plus everything nested under it), children, and implicit -- true for a level that exists only because something deeper is filed under it. Read subtree to pick the scope worth warming up: a parent holding nothing of its own can still be where the work is.

also and subtree_also are the same two counts for memories CROSS-LISTED here rather than filed here -- the cross-cutting subjects. A path with count 0 and also above it is one of those and nothing else: an end-to-end flow whose steps all live under other branches. Reads scoped to it return them all.

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 domain returns it from now on, without moving it -- use the dashboard's re-home for that.

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 domain_adjusted note). Read this before coining a new domain so its casing matches what will be stored.

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 est_tokens, the estimated cost of a record's FULL content: on a truncated one that is what the get_memory(uid) would cost, on latest_checkpoint it is what this response already spent. diagrams has no bodies to price.

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 -- scope.paths reports which, and an ambiguous name resolves to ALL of them.

scope is the rest of the brief: what the scope HOLDS, next to what came back. This is a warm-up, so each list stops at a handful and the newest few of a busy child can fill it on their own -- scope.not_shown counts what that left behind, per type, and scope.subdomains says which level it is sitting in (own = filed there, subtree = with its descendants). Read them as the drill-down plan: search(query, domain=...) or list_by_domain(domain, type=..., limit=...) on the child that holds what this pass only counted. A pulse is the state of a scope, never its contents.

scope.stale is the one thing here about DECAY rather than contents: how many memories in the scope carry a review_after date that has passed. Present only when non-zero. It means somebody who knew the subject said when to look again and nobody has -- optimize_scan lists which ones, with their source_ref.

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. scope.also counts how much of the brief arrived that way, and a subdomain carries its own also -- present only when non-zero, so a store that never cross-lists never sees the field.

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 referenced_by_diagrams, the flows that point a step at it -- so a note tells you which processes depend on it without a second lookup.

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 new_content as a new line at the end instead of replacing the body. Use it when a memory gains a fact rather than turning out to be wrong: the alternative is reading the whole thing, restating it and sending it back, which pays for the body twice and stakes the existing text on it being copied faithfully. Append what THIS memory gained. A fact about a further subject is a new memory plus an edge, not a line at the bottom -- appended text is ranked as part of the body it lands in and comes back with it.

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 new_content, for the common case of a body that is right and a reference that is missing or has moved; an empty source_ref leaves the stored one alone, and clearing one is a dashboard edit. Passing neither is an error rather than a silent no-op.

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 reason is recorded as a status-change audit entry, without touching the content.

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.

uids is comma-separated, domain a path (its subdomains and archived rows go too); give either or both. Each memory travels whole -- body, cross-listings, usage counts, edit history, the relations and diagram graph inside the slice -- into target, is checked there, and only then purged from the active project, after a backup of it is written.

dry_run is the default and moves nothing: it reports what would move, conflicts (uids target already holds, which stay here) and outside -- the relations, diagram links and jumps, superseded_by marks and [[uid]] references that cross the edge of the slice, all of which the move drops. Read that report with the user, then widen the slice or accept the loss BEFORE calling again with dry_run=False: the purge is irreversible short of the backup. create makes a target that does not exist yet. list_projects() names the projects there are.

dedup_scanA

Surface likely-duplicate/contradictory memory pairs.

Lexical overlap over near-identical text -- each pair carries its method. Two takes on one subject in different words do not surface here. Same-domain/session checkpoint pairs are excluded (timelines, not dups) and checkpoint pairs rank below durable-type pairs. Not an automatic merge -- returns candidate pairs + similarity score for the agent to review and decide (link_memories / edit_memory / forget as appropriate).

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 anchors (URLs, paths, identifiers) to check against live facts. Read it, then stage what you decided with optimize_stage.

Start with what the store already says is suspect: due: true on a memory means its own writer dated it for a recheck and the date has passed, and source_ref says what to check it against.

recalls and stats.never_recalled are NOT that. A low count means unproven, not useless -- a memory about a rare subject looks exactly like a memory nobody wants, and the rare subject is frequently the reason the store is there. Use the aggregate to judge the STORE and never a single row: do not propose archiving something because it is unread.

The listing is slim so a big store fits one response, and a page ends early at an internal size budget -- truncated means page onward with offset + count. since limits the scan to a delta for recurring passes; full=True keeps whole bodies.

BEFORE PROPOSING ANY CHANGE, CHECK IT AGAINST LIVE FACTS, and record what you checked in each suggestion's verified. Destructive kinds are rejected without it. help(command='optimize_scan') has the rest: what every hint means, how since stays cross-window, and what "live facts" covers per memory type.

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 verified describing the live-facts check behind them. Invalid suggestions are skipped and reported in errors; the rest are staged. Returns {run_id, staged, errors}.

note is one short summary of the pass, at most 250 characters; a longer one raises and stages nothing. What a single suggestion needs said belongs in its own rationale and verified, which are not capped. help(command='optimize_stage') explains each kind in full.

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

NameDescription
warm_upWhat 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

NameDescription

No resources

TDQS

A3.8/5.0

Scored across 38 tools

Disambiguation4/5

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.

Naming Consistency4/5

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.

Tool Count2/5

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.

Completeness5/5

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.

Maintenance

ActivityActive
ResponsivenessNo issues