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.

Long-term memory that survives between sessions. Read before working: pulse(domain) for the state of a subject, recall(query) or search(query) for anything specific, list_domains() for the tree that exists. Write as you go, not at the end: note() a durable fact, anti_pattern() a pitfall worth not repeating, checkpoint() where the work stands before a pause, diagram() a routine start to end. One memory holds ONE fact: when a body grows into several subjects, write them as separate memories and link_memories() them to each other. A claim you could not check says so in its own body; set_confidence(uid, 'confirmed'|'contradicted') closes it once the evidence turns up. must_read() says which memories wait to be read in a scope -- open tasks first (task() files one), then pitfalls, handoffs, notes and flows; work an open task with task_item() and it closes itself once every item is done or dropped. A person never sees an item's id, and reads a task's goal, comments and notes in the dashboard: there an item is [[#id]], shown as its number, and anywhere else -- a reply, a file, a commit -- it is item n of its task, named by the task's title. A bare id is written nowhere. pinned in must_read() counts memories a person marked as mandatory reading: list them with must_read(type=..., pinned=true) and open every one before acting.

A domain is a path ('acme/x100/p200') and every read covers its subdomains, so the same call asks about a product or one routine depending on how much of the path it gives.

NOTE: no memai hook is registered in the user's settings, so nothing puts the store in front of this session -- memai is read only when you call it, and a session that forgets to call it starts blind. Say so in your first reply and offer to run this, exactly as written (the name alone is not on PATH):

/app/.venv/bin/memai-hook install

It registers SessionStart, PreCompact, Stop and PreToolUse in ~/.claude/settings.json, for every project -- the last of those is the guard: it refuses a memai write whose required text never arrived, and records the domains a session names, which scopes the task ask at Stop. --check reports what is registered, --print shows the block without writing it.

Capabilities

Features and capabilities supported by this server

Protocol revision2025-11-25

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

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')); must_read(type='note') lists the most recent ones as headers.

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'.

A domain holds ONE active checkpoint. While one is already filed at domain, the call is refused and nothing is written: the refusal lists it under checkpoints. Read it with get_memory(uid) and decide whether its work is done. Done: forget(uid, reason='...'), then call again. Not done: carry what still holds into these fields and call again with supersedes='', which archives it as superseded by the new one. A checkpoint past the age limit set in the dashboard refuses every write under its domain the same way, flagged overdue, until it is settled.

anti_patternA

Record a pitfall: a move that looks right, why it fails, and what to do instead.

For a mistake worth not repeating. A fact that simply holds is a note(); how an analysis ran and what it settled is a reasoning(). Stored as type='anti_pattern' and read back under TEMPTATION / WHY WRONG / INSTEAD; pulse() counts the open ones for a domain and must_read(type='anti_pattern') lists them.

ONE pitfall per memory: a second temptation from the same session is its own anti_pattern(), connected with link_memories().

Returns the new uid and the project it landed in, plus similar when the store already holds something close: correct, drop or link it as similar_hint says. A field over its ceiling is refused and nothing is written.

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

taskA

Open a task: a goal and a checklist, kept until its items are closed.

Stored as type='task'. Work it with task_item(), grow it with task_add() and discuss it with task_comment(); it archives itself once every item is done or dropped, and must_read() lists the open ones.

Free text cites another item only as [[#id]], which reads back as its current number and label; a label -- a title, an item's text -- never carries one. To a person, name it item n of its task.

task_itemA

Update one item of a task -- its text, its state, a comment on it, memories linked to it -- or delete it.

Give at least one of text, state, comment and related, or delete. They apply together, or not at all.

The result names the item's number n and the task's title.

Free text cites another item only as [[#id]], which reads back as its current number and label; a label -- a title, an item's text -- never carries one. To a person, name it item n of its task.

task_addA

Append items to a task, one per line; a closed task reopens.

For work the checklist is missing. To move or comment on an item that exists, task_item(); to give an item its detail, task_note().

Free text cites another item only as [[#id]], which reads back as its current number and label; a label -- a title, an item's text -- never carries one. To a person, name it item n of its task.

Returns {"uid", "items", "progress", "task_state", "archived"}: the new items as {id, n}, and the task as it stands.

task_commentA

Comment on a task, or on one of its items when item names its id.

A comment never edits the task's content, so it carries what the checklist cannot: why an item is blocked, what a review said. Text an item is worked from -- its brief, a rule several items share -- is a task_note(); a comment that goes with a change of state goes in task_item()'s comment.

Free text cites another item only as [[#id]], which reads back as its current number and label; a label -- a title, an item's text -- never carries one. To a person, name it item n of its task.

Returns {"uid", "comment_id"}, plus item and n on an item.

task_readA

Read one collection of a task, one page at a time.

Follow next_offset until it is absent.

task_noteB

A note a task owns: an item's brief, a rule its items share.

Not a memory: only task_read() returns it. A note on items is a brief: goal, context, steps, pitfalls, done_when and depends_on are required, extra_info is optional, and they read back under the labels GOAL: / CONTEXT: / STEPS: / PITFALLS: / DONE WHEN: / DEPENDS ON: / EXTRA INFO:. A note on the whole task is free text in body.

Free text cites another item only as [[#id]], which reads back as its current number and label; a label -- a title, an item's text -- never carries one. To a person, name it item n of its task.

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.

review_after and source_ref matter most for a flow: 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.

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.

Calling again with the same endpoints updates the label instead of adding a second edge between them.

diagram_linkA

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, and to say the branch continues in another flow, diagram_jump().

get_memory() on the linked memory reports the diagrams that reference it, so the connection is visible from both ends.

Returns {"ok": True}, or {"ok": False, "errors": [...]} for an unknown diagram, step or memory, a link from a diagram to itself, or a delete with no such link.

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.

Stored once and read from both ends, so the return trip already exists and get_diagram(format='json') reports it on both diagrams.

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: when the user asked to SEE the flow, reading the file and emitting it inline is the work, not a cost to avoid.

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.

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.

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.

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. This (or search(type='note')) is how notes come back by relevance; must_read(type='note') lists the newest.

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.

Results carry the same succeeded_by / collapsed annotations search() explains.

list_by_domainA

List memories for a domain and its subdomains, most recent first.

Fallback when search misses. A task row carries state (open, completed or cancelled) and progress {done, total}.

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.

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; 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": [...]}, both lists 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, and archived records are left out of it, 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.

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 for every domain written from now on.

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": ...}.

must_readA

What is still open in a scope: counts per category, or one category's headers.

Without type: {"categories": [{"type", "count"}, ...]} for task, anti_pattern, handoff, note and diagram, in that order, leaving out the empty ones, plus "pinned" -- the same shape, counting the memories a person pinned as mandatory reading -- when any pin is in scope. A pinned memory still counts in its category. With type: {"type", "total", "items", "next_offset"} -- one page of headers (uid, title, domain, tags, est_tokens; a task adds progress {done, total} and doing, its items in progress as {id, n}), newest first, a task by its latest item update. Open a header with get_memory(uid). A page holds at most limit headers and ends early at the size budget; next_offset is absent on the last page.

Every pin listed is read with get_memory(uid) before acting, none skipped.

Pending means: a task that is open; an active anti_pattern, handoff or note that is not contradicted; an active diagram.

overdue, present only when non-empty, lists the checkpoints in scope that stayed open past the age limit, with an alert to relay to the user: memai refuses new writes under each until it is settled.

pulseA

Session warm-up: the latest checkpoint, what is pending, and what to read next.

Returns {project, latest_checkpoint, must_read, pinned, read_next, scope}, plus overdue when a checkpoint in scope stayed open past the age limit -- the same block must_read() carries, and read_next then puts settling it first.

latest_checkpoint is picked 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. It 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, and carries est_tokens, what this response already spent on it.

must_read is the categories list must_read(domain) returns without a type: a count per category (task, anti_pattern, handoff, note, diagram), leaving out the empty ones. read_next is the instruction to follow with it, before acting: pinned memories, then open tasks, then must_read(domain, type=...) for each other category, numbered when there is more than one step. It is "" when nothing is pending. A pulse lists no memories besides the checkpoint; must_read(domain, type=...) lists headers and get_memory(uid) opens one.

pinned counts the pins in scope the same way (see must_read()); when it is non-empty read_next asks for them before anything else.

scope is the rest of the brief: what the scope HOLDS. scope.by_type counts every memory 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 you need. 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.

A long checkpoint is cut (next names the rest).

get_memoryA

Fetch one memory: its fields, and counts of what is linked to it.

next names the call that pages each part: get_relations() for relations and diagrams, get_diagram() for a diagram's graph, task_read() for a task's items, notes and comments, edits_offset for the edit history and content_offset for a body too long for one response.

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. Each field is settable on its own; passing none of new_content, source_ref, title, tags and goal is an error rather than a silent no-op.

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. A task's content is generated from its goal and items the same way: rewrite its goal with goal, and its items through task_item().

link_memoriesA

Create a queryable edge between two memories.

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 a memory's links, one page at a time; follow next_offset.

set_confidenceA

Set a memory's confidence: unverified | confirmed | contradicted.

Call it once evidence settles a claim: the code, a run or a source checked against the memory. A contradicted memory sorts last in search and is left out of the notes, pitfalls and handoffs must_read() lists and of the checkpoint pulse() returns, so a claim known to be wrong stops steering work while it stays readable; unverified and confirmed rank alike. To correct the text, edit_memory(); to retire it, forget().

Now and then a result of any memai tool carries confidence_reminder: memories still unverified past the span set in the dashboard, those in the call's domain first. Settle the ones this session's evidence reaches; it refuses nothing.

Returns {"ok": True}, {"ok": False} for an unknown uid, or an error for any other value.

forgetA

Archive a memory (soft delete -- content is kept, just excluded from default search/list).

The call for a memory that is wrong, stale or a copy. It is reversible: the dashboard's restore brings an archived memory back, and list_by_domain(status='archived') still reads it. purge_memory() is the irreversible delete, only on the user's explicit request; to correct a memory instead of retiring it, edit_memory(). Archiving an open task cancels it; its items keep their states.

Returns {"ok": True}, or {"ok": False} for an unknown uid.

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.

move_to_projectA

Carry memories from the active project into another one, and remove them here.

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.

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

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. relations_truncated means the page's first memory has more edges than fit: get_relations(uid) lists them all. stats.by_domain names the largest domains; stats.domains counts every one.

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: cross-check newer memories in the corpus, verify code anchors against the live repo, web-check world facts.

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.

Invalid suggestions are skipped and reported in errors, as many as fit one page, with errors_total counting them all when some are left out; the rest are staged. Returns {run_id, staged, errors}.

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.

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.6/5.0

Scored across 43 tools

Disambiguation3/5

The core write verbs (note, checkpoint, anti_pattern, reasoning) and the task_/diagram_ families are well separated, but the read surface overlaps heavily: recall is explicitly 'sugar for search(type="note")', and list_recent, list_by_domain, must_read, and pulse all return 'most recent memories' with only subtle scope differences. dedup_scan and optimize_scan also cross into the same 'find suspect memories' territory. Descriptions do a lot of work to disambiguate, but an agent still faces real misselection risk.

Naming Consistency3/5

Within domains the prefixes are consistent (task_add/task_read/task_note, diagram_node/diagram_edge), but across the server conventions clash: bare nouns (note, checkpoint, task, diagram), bare verbs (search, forget, pulse), verb_noun (get_memory, list_domains), and noun_verb (task_add, diagram_link) all coexist. Generally readable, but not a single predictable pattern.

Tool Count2/5

43 tools is a very heavy surface for a memory store and pushes well past the 'too many' threshold. Several clusters could be collapsed (four optimize_* tools, four separate diagram-edge/link/jump verbs, near-duplicate read tools), so the count feels inflated relative to what an agent can reliably choose among.

Completeness5/5

Coverage is genuinely thorough: create/read/update/delete across multiple memory types, soft vs hard delete, relations, edit history/versioning, confidence, domain and project management, plus a full curation workflow. Almost no obvious lifecycle gap remains for a memory-management domain.

Maintenance

ActivityActive
ResponsivenessNo recent community issues