Skip to main content
Glama
Cloto-dev

CPersona

Official
by Cloto-dev

Server Configuration

Describes the environment variables required to run the server.

NameRequiredDescriptionDefault
CPERSONA_RRF_KNoRRF smoothing parameter60
CPERSONA_DB_PATHNoSQLite database path./cpersona.db
CPERSONA_SEARCH_MODENoSearch strategy (`rrf` or `cascade`)rrf
CPERSONA_EMBEDDING_URLNoEmbedding server URLhttp://127.0.0.1:8401/embed
CPERSONA_AUTO_CALIBRATENoAuto-calibrate on startupfalse
CPERSONA_EMBEDDING_MODENoEmbedding mode (`http` or `disabled`)http
CPERSONA_CONFIDENCE_ENABLEDNoInclude confidence metadata in resultsfalse
CPERSONA_TASK_QUEUE_ENABLEDNoEnable background task queuefalse
CPERSONA_VECTOR_SEARCH_MODENoVector search moderemote

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
}
experimental
{}

Tools

Functions exposed to the LLM to take actions

NameDescription
pause_persistenceA

Pause write operations on this MCP server for an opt-in TTL window (default 1800 seconds, at most 86400). While paused, every write tool — store, declare_associations, archive_episode, update_memory, delete_memory, delete_episode, delete_agent_data, lock_memory, unlock_memory, update_profile, import_memories, merge_memories, calibrate_threshold, set_recall_precision — returns a no-op response carrying persisted: false, dry_run: true and a reason (with the TTL remaining) instead of writing. Branch on persisted: false, not on an id: an id reads "no-persist" (store, archive_episode), and the action-specific id keys (deleted_id / updated_id / locked_id / unlocked_id / episode_id) are null. migrate_channel_axis is instead forced to dry_run and reports repairs_skipped, with no persisted key; check_health and deep_check downgrade to fix=false (repairs_skipped: true). Read tools answer normally, but recall does not bump recall_count / last_recalled_at. Blast radius follows session_key (response scope). Pass the same session_key here and on your write calls and the pause covers that key alone (scope: "session"): a session that sends a different key is neither silenced by it nor able to clear it. The key is a partition hint, not a credential, so anyone who sends the same string shares the pause. Omit it and you arm the bucket every keyless caller shares (scope: "process") — on a streamable-HTTP deployment one process serves every client, so a keyless pause silences writes for every other keyless session until resume or TTL elapse, and those sessions get no signal. Under stdio (one process per client) that bucket is the session. This affects only cpersona; pause cscheduler separately.

resume_persistenceA

Re-enable persistence immediately, clearing this caller's active no-persist TTL. Returns was_active=true if THIS bucket was paused before the call. It clears only the bucket session_key selects (response scope): with a session_key, your own pause and no other session's; without one, the shared keyless bucket, which on a streamable-HTTP deployment re-enables writes for every other keyless session too.

persistence_statusA

Report whether persistence is currently paused and the TTL remaining (in seconds). It reports the bucket session_key selects (response scope), not the server as a whole: with a session_key it answers for your session only, so paused: false here does not mean no other session is paused. Without one it reflects the bucket every keyless caller shares, which on a streamable-HTTP deployment means paused: true may have been armed by a different keyless session.

get_operating_contextA

Read the server-served operating context (v2.5.1): the operator-owned doctrine distributed to every connected client. Without arguments returns the preview tier — context_revision, instructions_summary, project_id registry (+ enforce mode), @auto defaults, and doctrine section names. Pass section to fetch one section's full body. Read-only: the context is edited by the operator on the filesystem (~/.cpersona/operating-context.toml), never via MCP.

storeA

Store a memory for future recall: put the text in content. agent_id may be omitted on a connection that can write to exactly one agent (the agent used is echoed as resolved_agent_id), and source defaults to that agent. lock:true also locks the memory, as lock_memory does, and the response says locked:true|false. Every response carries result, the one field to branch on: 'stored' (a new row: id, and embedded, whether a vector was kept; truncated:true when content was shortened to the length cap; nodes:{status:'queued'} when the text runs past the embedding window and its overflow-tree nodes were queued), 'skipped' (nothing written and nothing wrong: a duplicate id, or content already stored under the same source id, echoes the existing row's id, except reason='duplicate (unique index)', a concurrent-write race, which has none), or 'rejected' (ok:false, nothing written; reason is human-readable, not a stable token). Under pause_persistence the write is skipped and the response carries persisted:false (id:'no-persist'), which tells a paused write from a duplicate. The older message={content, id, source, timestamp, metadata} form still works; use it or the top level, not both.

declare_associationsA

Declare associative memory after the fact: entities with aliases, and subject–predicate–object relations, recorded verbatim and walked by reconstruct. The server extracts nothing and infers nothing — coverage is exactly what was declared. anchor_ref names the record (mem:<id> / ep:<id>) the declaration is evidenced by: every entity named is recorded as mentioned by it and every relation carries it. Malformed items are reported in dropped and skipped; nothing else in the call is refused for them. retract removes relations by id and mentions by {entity, ref} — the only way a declaration leaves the store. Response: {ok, result:'declared', entities:[{id, name, created}], mentions, relations:[ids], dropped:[{item, reason}], retracted?:{relations, mentions}}. Under pause_persistence nothing is written (result:'skipped', persisted:false).

traverseA

The neighbourhood of a declared entity, as a graph: the entity named, its aliases, the entity -> entity relations declared on it and on what they reach, up to max_hops in either direction, and the refs of the records that mention each entity. Only what was declared (declare_associations, or associations on store); nothing is inferred. No record text: expand a ref with get_contents. entity is a name or an alias, compared after normalization; when it names more than one entity this call can read (a project's and the global pool's), all are starts. ORDER: entities by hops, then by the most recently declared relation that reached them, then by id; mentions by record id; relations most recently declared first. limit bounds both the entities returned and the refs listed per entity. Response: {entity, max_hops, limit, entities:[{id, name, hops, aliases?, mentions?, mentions_omitted?}], relations:[{id, subject, predicate, object, declared_by, declared_at, anchor_ref?}] (subject/object are entity ids from entities; a relation is listed when both ends are), entities_omitted?, bounds?:{omitted:[max_hops | limit]}, reason?:'no_such_entity'}. Relation ids are what declare_associations' retract takes. Records are listed only when this call could read them: the project, channel and source_id filters apply as in recall.

recallA

Recall memories by multi-strategy search (vector + FTS5 + keyword). To answer a question from memory, prefer reconstruct: it returns items that quote the rows supporting them, within a character budget. Rows come in ascending order of relevance: the LAST row is the best match (docs/behavior-contracts.md section 1). gate_fallback: true (absent otherwise) means every candidate fell below the quality gate and the below-gate lexical matches were returned: treat them as low-confidence. That rescue runs only with CPERSONA_CONFIDENCE_ENABLED=true and CPERSONA_CONFIDENCE_ORDERING=legacy; otherwise an all-below-gate recall is empty. suggestion (absent otherwise, at most once per session) is something only the user can decide: that this scope has outgrown the scan window with no coarse index for a time cue to search the rest. Relay its message, and run its fix only if the user agrees. Content is a preview of the record's start by default. A row the preview cut also carries excerpt, the part of the record that matched the query (at most 800 characters, CPERSONA_RECALL_EXCERPT_CHARS, 0 = none; passages joined by ' … ' in text order), and excerpt_basis (blocks / lexical / start). Read the excerpt before expanding a row with get_contents(refs). full_content=true returns whole rows, capped at 200,000 characters per response: rows past it fall back to the preview and the response carries full_content_budget_chars. Each scored row carries match_reason {signal, score, ...}: signal is the branch the quality gate keyed on (rsf > cosine > rrf), the other keys are the per-retriever contributions, and prior is the age weight when one ordered the row. An enabled confidence score is returned beside each row but neither orders nor gates it (unless CPERSONA_CONFIDENCE_ORDERING=legacy). Unscored rows (cascade FTS/keyword) have no match_reason.

recall_with_contextA

Recall memories and merge with external conversation context. Automatically deduplicates, sorts chronologically, and returns a unified list. Replaces separate recall + manual merge in the caller. Content is preview-tiered by default — see recall's full_content / get_contents (full_content shares recall's 200k-char response budget, bug-211); a recalled row the preview cut carries excerpt / excerpt_basis as recall's do. Every external_context entry's content filters the recall (the caller already holds that text), but only role=user / role=assistant entries are merged into messages. When entries of other roles are present the response carries context_filter_only={roles:[...]} — those entries filtered the recall without appearing in the output, whether or not they dropped a memory this time. context_field_issues={entries:[{index, fields}]} (absent otherwise) names entries whose declared field was not a string: it was read as absent and the entry merged without it. CPERSONA_EXTERNAL_CONTEXT_MODE=reject refuses such a call instead. gate_fallback=true (absent otherwise) is forwarded from the underlying recall: every candidate fell below the quality gate and the below-gate lexical matches were returned — treat them as low-confidence. Only CPERSONA_CONFIDENCE_ORDERING=legacy with confidence on does that; otherwise such a recall contributes no rows.

get_contentsA

Fetch full, untrimmed content for recall preview refs. Use after a preview-tier recall to expand only the rows that matter instead of opting the whole recall out with full_content=true. Bounded twice: at most 20 refs per call, and a 40,000-character budget across the batch (2.5.4a2) that does not move when CPERSONA_MAX_CONTENT_LENGTH does. Rows are never cut to fit — when the budget is spent the remaining refs come back in deferred (absent otherwise) alongside budget_chars; re-fetch them in a second call. A single row larger than the budget is still returned in full, because this tool is the only path back to a row's complete text. RANGES: a ref may instead be an object that names part of its record -- {ref, node: i} or {ref, node: [first, last]} (inclusive) for overflow-tree nodes, e.g. the node.index of a reconstruct quote and its neighbours, or {ref, span: [start, end]} for characters. Offsets are in the stored text (a memory's content, an episode's summary without the '[Episode] ' label). The item then carries that slice as content and range = {span, content_len, and node + of when nodes were named, or block + of when blocks were}; a span end past the text is clamped and range.span says what was served. A ref may also carry revision, the digest a reconstruct quote's expand hands out for the text its offsets were measured in: a record rewritten since refuses rather than serving different characters under the same numbers. A range that cannot be served exactly is never widened to the whole row: it comes back in unresolved (absent otherwise) as {ref, reason}, reason one of invalid_range, no_current_nodes (the record has no complete node set -- short records have none, and a new long one gets them shortly after store), node_out_of_range, span_out_of_range, no_current_blocks, block_out_of_range, stale_revision. Only the slice counts against the budget.

reconstructA

The recommended way to answer a question from memory. Returns up to count ITEMS (10 by default): units of memory, each quoting verbatim its most relevant record. content is the parts of that record that matched the query; pass head_ref, or an item's expand as it is, to get_contents to read more. No model is called and nothing is summarised; stored rows are never modified. COUNT IS A CEILING, not a fill target or a search depth: fewer items is a normal result and carries shortfall_reason, and nothing is padded. reserved_count held items (block, cue, far, mode seats) may trail the window. Breadth is set apart from count: raise top_k when items are missing evidence; max_hops and max_evidence bound the declared-relation walk and the rows kept per item. BUDGET bounds the characters of quoted text (content plus excerpts), where count bounds items: excerpts are omitted before items, and raising the budget never removes an item or an excerpt. Omit it for the server default. mode (lite / pro) caps the tokens returned (see it). READ FURTHER IN STEPS, smallest first: an item's expand, then the neighbours of a quote's node or block index ({ref, node: [i-1, i+1]}, or block), and the bare ref, the whole record, last. ITEM: claims lists the retained rows newest first (ref, as_of, why, hops, roles); excerpts quote the other claims. roles[].role names what the REFERENCED row is to this claim (supports / supersedes / corrects / qualifies / contradicts / temporal_predecessor); ignore a role you do not know. conflicts appears only when two rows cannot be ordered. A response adds fields only when the server did something other than asked (count_policy, budget_policy, bounds, gate_fallback); their absence does not say the items suffice or that the whole store was searched. trace=true returns the full audit. Declared associations widen the lexical search and add walked evidence inside items, never items. Full contract: docs/RELIABLE_RECALL_2_6.md section 7.

get_profileC

Get the current profile for an agent.

update_profileA

Save a pre-computed agent profile to the database. The text passes through the same sanitizer as store, against the profile's own ceiling: it is capped at 2000 characters (CPERSONA_MAX_PROFILE_LENGTH) and the response carries truncated:true when the cap bit — branch on it, the discarded remainder is not stored anywhere else.

archive_episodeA

Archive a conversation episode with pre-computed summary, keywords, and resolved status. All LLM processing is performed by the caller. A summary that runs past the embedding window adds nodes:{status:'queued'} to the response, as on store.

list_memoriesA

List recent memories for an agent (for dashboard display). bug-385: limit is clamped to 500 rows. When the caller asked for more and rows past the cap exist, the response carries budget_rows (the cap), so a capped listing can be told from one that reached the end of the data — reach the rest through export_memories or a narrower filter, not a larger limit. bug-255: within that cap the response holds a 1,000,000-character content budget. Rows are returned newest-first and none is dropped by the budget; once it is spent, later rows LONGER than the preview cap (CPERSONA_RECALL_PREVIEW_CHARS, default 500) degrade to a pure prefix with content_truncated/content_len and a ref that get_contents expands under the row's own agent_id (in an all-agents listing, pair the ref with the row's agent_id field). budget_chars appears iff at least one row was degraded. The effective ceiling is the budget plus one whole row plus the degraded rows' prefixes, so it scales with the preview cap; preview cap 0 disables trimming and the budget with it.

list_episodesA

List archived episodes for an agent (for dashboard display). bug-385: limit is clamped to 200 rows. When the caller asked for more and rows past the cap exist, the response carries budget_rows (the cap), so a capped listing can be told from one that reached the end of the data — reach the rest through export_memories or a narrower filter, not a larger limit. bug-255: within that cap the response holds an 800,000-character budget across summary and keywords together, with the same degradation and ceiling semantics as list_memories — rows past the budget that exceed the preview cap carry pure prefixes plus summary_truncated/summary_len and keywords_truncated/keywords_len; budget_chars appears iff at least one row was degraded. Their ref expands the summary via get_contents (under the row's own agent_id); a full keywords string is only available through export_memories.

delete_agent_dataA

Delete ALL data (memories, profiles, episodes) for a specific agent. Used by kernel during agent deletion.

calibrate_thresholdA

Auto-calibrate the vector search threshold from the null (random-pair) cosine distribution. Samples random memory pairs and places the threshold ABOVE the null mean so unrelated pairs are rejected. method='separation' (default) learns the operating point from two populations — null pairs vs temporally-adjacent same-session positives (nearest-neighbour fallback when too few exist); method='percentile' uses a quantile of the null distribution (robust to anisotropic models such as bge-m3); method='zscore' uses mean + z*std. No labels used, purely statistical. Adapts to both embedding model and corpus characteristics.

set_recall_precisionA

Set an agent's recall precision (knob 3) and recalibrate its quality gate. precision = strict | balanced | lenient maps to a specificity weight beta of 2.0 / 1.0 / 0.5 in the gate separation objective (sensitivity + beta*specificity): strict sits the gate higher (fewer contaminants, more misses), lenient lower (fewer misses, more contaminants). A raw beta > 0 overrides the named level; an empty precision with beta <= 0 clears the per-agent override and returns the agent to the global CPERSONA_RECALL_PRECISION default. The gate is recalibrated at the new beta immediately and persisted, so the change is live without a restart. Precision is a per-agent setting, not a per-recall argument: the gate threshold is precomputed on the separation curve at a fixed beta, so this tool recalibrates once instead.

get_recall_precisionA

Read an agent's effective recall precision (knob 3) — the read-back companion to set_recall_precision. Returns the resolved specificity weight (beta) and its named precision level (strict / balanced / lenient, or 'custom' for a raw beta), and flags whether the value is a per-agent override or the global CPERSONA_RECALL_PRECISION default (overridden + global_precision / global_beta). Read-only: it never recalibrates and never persists, so a UI can load the current setting, let the user edit it, and write it back instead of the control being write-only.

delete_memoryB

Delete a single memory by ID. Ownership is enforced when agent_id is provided.

delete_episodeA

Delete a single episode by ID. Ownership is enforced when agent_id is provided.

update_memoryA

Update memory content by ID. Rejects if memory is locked. Ownership enforced when agent_id provided. The new content passes through the same sanitizer as store: it is capped at the content length limit (the response carries truncated:true when the cap bit) and [Memory from ...] annotations are stripped, so content consisting only of those is refused rather than written as an empty row. A new text that another memory in the same agent, project and channel already holds is refused with ok:false naming that memory, since one copy is kept per bucket. A new text that runs past the embedding window gets nodes:{status:'queued'}, as on store.

lock_memoryA

Lock a memory to prevent deletion and editing (delete_memory, update_memory). Operator re-scoping (migrate_channel_axis, merge_memories move) still relocates it, lock kept; delete_agent_data removes it. Ownership enforced when agent_id provided.

unlock_memoryA

Unlock a memory to allow deletion and editing. Ownership enforced when agent_id provided.

get_queue_statusA

Get the status of the background task queue (pending tasks, retry config).

export_memoriesA

Export memories, episodes, profiles and declared associations (entities, aliases, mentions, relations) to a JSONL file for backup or portability.

import_memoriesA

Import memories, episodes, profiles and declared associations from a JSONL file; the associations are pointed at the records they name as restored. Idempotent: memories deduplicate on msg_id (and on content within a project/channel), episodes on their summary within a project/channel.

merge_memoriesA

Merge memories, episodes, profiles and declared associations from one agent into another. Atomic one-shot equivalent of export→import without intermediate files. Strategy 'skip' deduplicates by msg_id (memories) and summary (episodes).

check_healthA

Check memory database health (38-check registry, each issue tagged critical/warn/info): contamination, duplicates, oversized content, embeddings, FTS and schema integrity, SQLite file integrity, naming, JSON and timestamp drift, stale tasks, profiles and sources. Returns storage stats with project_id/channel distributions. The verdict is status (healthy / degraded / unhealthy, from severity counts; info never degrades). Read it as a verdict on what is IN the database, not on whether the pipeline that fills it is working: a corpus whose every embedding is NULL scores healthy while semantic recall is dead. Only fix=true judges the backend's liveness: a report-only run still contacts it (a probe embed, its identity, token counts) but reads none of that as liveness, so a quiet report-only run is not evidence the backend answered; the null_embedding finding says why its repair cannot run. checks runs a subset: an unknown name is rejected (ok=false), and every response echoes checks_run. fix=true repairs, agent-scoped and locked-safe, except block_index, coarse_index, dedup_msg_id_index, file_permissions, whose repairs reach every agent, so under an ACL they need read-write on '*'; leave them out of checks to stay agent-scoped. critical file-integrity findings are report-only. Lossy, irreversible repairs: oversized memories are cut to CPERSONA_MAX_CONTENT_LENGTH (default 16000) and the profile to CPERSONA_MAX_PROFILE_LENGTH (default 2000), keeping the start (lowering a cap shortens rows that fit the old one); duplicate_content and empty_content delete rows; memory_annotation and discord_mention strip text; invalid_json blanks unparseable JSON to '{}'; stale_pending_tasks deletes queued writes over an hour old; dedup_msg_id_index blanks colliding msg_ids; timestamp checks copy created_at over bad stamps. Some repairs are bounded per run (source canonicalisation classifies at most 10000 rows): a fix response with remaining > 0 has NOT converged; run fix again until remaining stops decreasing.

get_session_findingsA

Pull the storage-integrity findings on demand (the SuperAuditor pull contract, docs/SUPERAUDITOR_STANDARD.md) instead of reading them off check_health: the same detector as check_health(fix=false) over the WHOLE database. Read-only, never repairs. Each finding carries kind (a check registry name, or an escalation tier this seam mints for a runner that grades its own severity, e.g. null_embedding_pipeline_down — a tier is NOT a registry name), check (the registry name that produced it: check_health(checks=[finding['check']]) re-runs exactly that probe) and a static per-kind severity (critical = the read contract is broken now / warn = two stored facts contradict / info = an observation). check_health's own verdict rides along as health_severity; a probe that raised is reported as kind check_crashed instead of failing the pull. NOT free: the registry runs unfiltered, including two whole-database reads (the FTS5 integrity-check over both indexes and PRAGMA quick_check), so every pull is O(database); budget it by call frequency. There is deliberately no cheap subset. Findings are NOT filtered by agent_id or project_id: slicing them by the caller's bucket would hide exactly the forgotten rows (scope a repair with check_health(agent_id=...)). Honest caps: findings holds at most per_kind_limit rows per kind, capped_kinds names every kind that had more, and total and the counts describe the RETURNED set only. summary restates it in prose (include_summary=false skips it). Without a session_key on a shared remote transport the response carries identity_shared: true; _meta.server_version identifies the running instance.

deep_checkA

Deep heuristic analysis of memory data quality. Detects issues requiring recovery or judgment (anonymous sources, short/trivial content, stale profiles, orphaned episodes, stale threshold calibration, embedding-space near-duplicate pairs as merge candidates). fix=true applies repairs for: anonymous_source, short_content. The short_content repair deletes every unlocked row it reports; run without fix first and read its samples. Report-only (fix is accepted and ignored): stale_profile, orphaned_episodes, calibration_staleness, near_duplicate, unnormalized_content, embedding_norm — apply those decisions via merge_memories / delete_memory / calibrate_threshold / update_profile. Use checks parameter to select specific checks.

migrate_channel_axisA

Re-channel bridge-type memories to their concrete channel (knob2 v2 default flip prep). Memories the kernel filed under the bridge type ('discord') are rewritten to the concrete channel recovered from the stored session_id ('{channel_id}:{user_id}:{chunk}' | '{channel_id}:shared' → channel_id), so per-channel recall can match them. Non-destructive (only the channel column changes) and idempotent (re-running is a no-op once moved). dry_run=true (default) reports the recoverable count, the channels that would be recovered, and an unrecoverable bucket (channel='discord' rows with no snowflake session_id) without mutating. globalize_unrecoverable=true moves the unrecoverable bucket to channel='' (global, matched by every channel-scoped recall) so the flip orphans nothing; default false (report only).

check_updateA

Report whether a newer release of this server exists, and — only if you ask — install it. The check itself runs ONCE per process start, in a background task that nothing waits on, and its verdict is cached for 24h (CPERSONA_UPDATE_CHECK_INTERVAL_SECONDS) in a file beside the database; a bare call here reads that verdict and reaches neither the network nor the disk. state is one of: ok (running the newest release) / newer (a newer final release exists — pre-releases are never proposed) / yanked (every file of the RUNNING version has been withdrawn on PyPI; reason carries the publisher's text) / unlisted (this version is not on the index at all — a development checkout; not a defect) / unknown (no check has completed, e.g. no network) / disabled. install names how this process was installed (uvx / pip / checkout / unknown) and the exact command that would update it. refresh=true performs the fetch now (3s budget) and updates the cache. apply=true runs that command as an argv list (never a shell), returning exit_code and the last 40 lines of output — supported for pip and checkout installs only; under uvx the environment is a cache entry keyed by the launch arguments, so the update belongs in your client's config (uvx cpersona@latest), and an install here would be discarded on the next launch. A checkout parked at a tag (detached HEAD) is likewise refused before anything runs, and answers with the git fetch --tags && git checkout <tag> form to use instead (install.command shows it too). One apply runs at a time; a second is refused until it ends. Updating is NEVER automatic and never a side effect of any other call. A RESTART IS ALWAYS REQUIRED afterwards: this process keeps serving the old code until it is replaced. Unaffected by pause_persistence — an install writes no memory row, so a no-persist session can still repair a withdrawn version. Set CPERSONA_UPDATE_CHECK=false to disable the feature entirely: no fetch, no cache, no notice on recall or check_health, and this tool answers state=disabled.

Prompts

Interactive templates invoked by user choice

NameDescription

No prompts

Resources

Contextual data attached and managed by the client

NameDescription

No resources

TDQS

A3.6/5.0

Scored across 34 tools

Disambiguation4/5

Most tools target distinct resources or actions, but several families have subtle boundaries: recall, recall_with_context, and reconstruct all retrieve memories, while check_health, deep_check, and get_session_findings all report diagnostics. The detailed descriptions explicitly steer between them, but an agent must read carefully to choose correctly.

Naming Consistency4/5

All tool names use snake_case and most follow a verb_noun pattern such as get_profile, update_memory, delete_episode, and check_health. A few are bare verbs (store, recall, reconstruct, traverse) or noun-like (persistence_status), so the convention is readable but not perfectly uniform.

Tool Count2/5

At 34 tools, the surface is well above the typical well-scoped range, mixing core memory operations with many niche admin, diagnostic, and maintenance tools such as check_update, get_queue_status, migrate_channel_axis, and pause/resume persistence. The breadth may reflect a mature backend, but it is heavy for an MCP server and increases selection cost.

Completeness4/5

The core memory lifecycle is well covered: store, update, delete, lock/unlock, list, recall/reconstruct, profiles, episodes, declared associations, import/export/merge, and health diagnostics. Minor gaps remain, such as no dedicated get/update for a single episode and no queue-management operations, but agents can work around them.

Maintenance

ActivityActive
ResponsivenessUnresponsive