MCP Tools
engram exposes its memory API as Model Context Protocol (MCP) tools. All tools
require an active MCP session. When OIDC is enabled, every call must carry a
valid bearer token; the verified identity becomes the actor and owner of any
records created.
Tool summary
Section titled “Tool summary”| Tool | Purpose |
|---|---|
store_memory |
Persist a deliberate, well-formed memory |
schedule_memory |
Persist a memory with a validity window (deferred reveal / expiry) |
search_memory |
Semantic search within a scope |
list_memory |
Most-recent memories in a scope (no query — session bootstrap) |
list_scheduled |
List windowed memories the recall gate is hiding |
get_memory |
Fetch one memory by id |
supersede_memory |
Correct a memory with a new record, preserving history |
update_memory |
Replace a memory’s content in place (re-embeds) |
delete_memory |
Delete one memory by id |
delete_all |
Delete your own memories in a scope (teardown) |
store_discovery |
Cache citation-backed codebase understanding |
search_discovery |
Semantic search over the discovery pool |
set_visibility |
Share or unshare a memory you own |
store_rule |
Persist a normative, user-blessed rule (ground truth) |
list_rules |
List the complete rule set for one or more scopes |
archive_memory |
Retire a memory you own without deleting it (reversible) |
restore_memory |
Reverse an archive_memory call |
related_memories |
On-demand neighbourhood: supersession, shared tags, shared citations, vector similarity |
list_tags |
Exact tag counts for a scope, for tag reuse before store_memory |
actor and owner are always server-set. They come from the validated OIDC
token and are never accepted as client input.
A rejected call — a missing or malformed argument, a bound exceeded — returns a field-and-hint envelope rather than a plain message; see the error reference for the full grammar and hint-code vocabulary.
Blast radius
Section titled “Blast radius”Every tool advertises four MCP ToolAnnotations hints — readOnlyHint,
destructiveHint, idempotentHint, openWorldHint — so an agent can read a
tool’s blast radius before calling it, never by triggering it first. Values
come from one shared table (internal/surfaces), generated here rather than
hand-maintained per tool; openWorldHint is false on every tool, since
engram is a closed memory domain. These are hints, not an authorization
mechanism — never make a tool-use decision based on annotations from an
untrusted server.
| Tool | readOnlyHint |
destructiveHint |
idempotentHint |
openWorldHint |
|---|---|---|---|---|
store_memory |
false | false | false | false |
schedule_memory |
false | false | false | false |
search_memory |
true | false | true | false |
list_memory |
true | false | true | false |
list_scheduled |
true | false | true | false |
get_memory |
true | false | true | false |
update_memory |
false | true | true | false |
delete_memory |
false | true | true | false |
delete_all |
false | true | true | false |
store_discovery |
false | false | false | false |
search_discovery |
true | false | true | false |
set_visibility |
false | false | true | false |
supersede_memory |
false | false | true | false |
store_rule |
false | false | false | false |
list_rules |
true | false | true | false |
archive_memory |
false | false | true | false |
restore_memory |
false | false | true | false |
related_memories |
true | false | true | false |
list_tags |
true | false | true | false |
store_memory
Section titled “store_memory”Persist a deliberate, well-formed memory. Do not store transient state, secrets, or timestamps.
| Argument | Type | Required | Description |
|---|---|---|---|
content |
string | yes | The memory text to persist. Max ENGRAM_MEMORY_MAX_CONTENT_BYTES bytes (default 65536; see Configuration). |
scope |
string | yes | run:tier:repo identifier, e.g. eval-2026-05:project:selfhosted-cluster |
source |
string | yes | user-said or agent-inferred |
category |
string | yes | decision, preference, convention, or gotcha |
tags |
string[] | no | Free-form labels. At most ENGRAM_MEMORY_MAX_TAGS tags (default 128) of at most ENGRAM_MEMORY_MAX_TAG_BYTES bytes each (default 128). |
repo |
string | no | Repository name or URL |
workspace |
string | no | Workspace identifier |
worktree_path |
string | no | Path to the git worktree |
base_dir |
string | no | Base directory for the project |
summary |
string | no | Short human-readable summary (caller-authored, summary_source=client). Max ENGRAM_MEMORY_MAX_SUMMARY_BYTES bytes (default 512; see Configuration). Omit for no summary. |
citations |
citation[] | no | Optional structured source anchors (same shape as store_discovery’s citations, max 50); never inferred — only what you explicitly supply. Omit for none. |
Returns the stored record’s id and short_id.
schedule_memory
Section titled “schedule_memory”Persist a memory with a temporal validity window.
schedule_memory requires not_before and/or not_after (use store_memory for unscheduled records). not_before must be strictly before not_after. discovery is not schedulable; use store_discovery.A future not_before hides the record from recall until then;
not_after drops it from recall at that time. Active windowed records surface
normally via search_memory/list_memory.
| Argument | Type | Required | Description |
|---|---|---|---|
content |
string | yes | The memory text to persist. Max ENGRAM_MEMORY_MAX_CONTENT_BYTES bytes (default 65536; see Configuration). |
scope |
string | yes | run:tier:repo identifier |
source |
string | yes | user-said or agent-inferred |
category |
string | yes | decision, preference, convention, or gotcha |
tags |
string[] | no | Free-form labels. At most ENGRAM_MEMORY_MAX_TAGS tags (default 128) of at most ENGRAM_MEMORY_MAX_TAG_BYTES bytes each (default 128). |
repo |
string | no | Repository name or URL |
workspace |
string | no | Workspace identifier |
worktree_path |
string | no | Path to the git worktree |
base_dir |
string | no | Base directory for the project |
summary |
string | no | Short human-readable summary (caller-authored, summary_source=client). Max ENGRAM_MEMORY_MAX_SUMMARY_BYTES bytes (default 512; see Configuration). Omit for no summary. |
citations |
citation[] | no | Optional structured source anchors (same shape as store_discovery’s citations, max 50); never inferred — only what you explicitly supply. Omit for none. |
not_before |
string | no | RFC3339; hide from recall until this time |
not_after |
string | no | RFC3339; drop from recall at this time |
Returns the scheduled record’s id and short_id. At least one bound is required. Operators
reclaim lapsed records with the engram prune-expired --apply CLI command (preview by default
without --apply; add --older-than DUR for a grace period).
Operators can also permanently delete purge-eligible records with engram spine-review purge --apply (preview by default without --apply), gated on an extract-before-delete precondition.
Its structural classes (superseded, expired, archived) need only that gate; the free-form
filter path (--category, --tags, or --older-than with no --class) additionally requires:
See the CLI guide for the full contract.
search_memory
Section titled “search_memory”Semantic (vector) search within a scope. Embeds query and returns the nearest
memories. By default returns compact summaries; pass full=true for complete content.
| Argument | Type | Required | Description |
|---|---|---|---|
query |
string | yes | Natural-language search query |
scope |
string | conditional | Scope to search within; scope is required unless cross_spine is true |
k |
uint64 | no | Number of results to return; 0 resolves to this tool’s default, 8; values above 1000 (the maximum) are rejected (field=k hint=out_of_range) |
tags |
string[] | no | Restrict to records carrying all listed tags (AND). Omit for no tag filter. Applied as a hard pre-filter, then results are ranked by vector similarity and a lexical-overlap adjustment selected by the retrieval eval (see below) |
categories |
string[] | no | Restrict to records in any of the listed categories (OR) — the opposite of tags’ ALL/AND semantics, since a record carries exactly one category. Omit or pass an empty array for no category filter. An unmatched value returns zero results, never an error; any stored category is accepted, including discovery and rule, not just the four store_memory write values. Applied as a hard pre-filter, before vector ranking. The same filter is available over the Connect read API on SearchMemories. |
created_after |
string | no | RFC3339 timestamp — include only records with created_at >= created_after (inclusive lower bound) |
created_before |
string | no | RFC3339 timestamp — include only records with created_at < created_before (exclusive upper bound). Half-open window: [created_after, created_before) |
full |
bool | no | Return full content instead of compact summaries (default false) |
cross_spine |
bool | no | Span all scopes the caller can read; ignores scope when true |
Returns a list of matching memory records. Each result carries a score: the
raw Qdrant cosine similarity for this query (higher = closer), present when
non-zero. Unranked list_memory/get_memory results have a zero/omitted score.
Final order may include a lexical-overlap adjustment selected by the
retrieval eval; score remains first-stage dense similarity and may be
non-monotonic after that adjustment. citations are omitted from the
default compact view; pass full=true to include them.
The result is returned as structured content and, per MCP 2026-07-28, also as
the same JSON in a text block.
On a cross-spine call (cross_spine=true), the response also carries
searched_scopes — every scope you can read that the search spanned, not the
scopes that produced hits — and scopes_truncated, true when that scope
enumeration hit its bounded ceiling and the list may be incomplete. Both keys
are omitted entirely on a scope-confined call, so an existing consumer’s
response shape is unchanged.
If the coverage enumeration itself fails after hits were already found, the
call still succeeds: scopes_unknown is true, searched_scopes is absent
(never an empty list, which would read as “searched nothing”), and
scopes_truncated is absent/false.
The response also carries recall_gate_hidden — {total, archived, superseded, expired, scheduled} counts of records the recall gate hid from
this same query at this same k, with the gate lifted, counting only states
this call did not already include. Present on a scope-confined call and a
cross-spine call alike (unlike searched_scopes, it is not gated on
cross_spine). Absent when the count could not be computed; all-zero when
nothing was hidden. A record hidden for more than one reason counts once in
total and once per state it carries, so the per-state fields can sum to
more than total. There is no argument to include hidden records in this
result set — fetch a specific one by id with get_memory.
With ENGRAM_SEARCH_RANKER=jev
enabled, results are instead reordered by the typed-decision provider’s
probability that each record answers the query — lexical order first, then a
stable sort by that probability — and each hit carries a per-hit relevance
value between 0 and 1 (values all near zero mean nothing returned actually
answers the query). Callers decide relevance for themselves from these
per-hit values; no hit is filtered out on the server’s behalf, and there is
no response-level flag. relevance is absent on every hit when the ranker is
off, and also absent (with score-based order unchanged) when a rerank
attempt fails — the search still succeeds, falling back to the default
lexical order.
list_memory
Section titled “list_memory”List recent memories in a scope without a query. Intended for session-start
bootstrap. Results are most-recent first. By default returns compact summaries;
pass full=true for complete content.
| Argument | Type | Required | Description |
|---|---|---|---|
scope |
string | conditional | The scope to list memories from; required unless cross_spine is true |
limit |
uint64 | no | Maximum memories to return; 0 resolves to this tool’s default, 20; values above 1000 (the maximum) are rejected (field=limit hint=out_of_range) |
tags |
string[] | no | Restrict to records carrying all listed tags (AND). Omit for no tag filter |
categories |
string[] | no | Restrict to records in any of the listed categories (OR) — the opposite of tags’ ALL/AND semantics, since a record carries exactly one category. Omit or pass an empty array for no category filter. An unmatched value returns zero results, never an error; any stored category is accepted, including discovery and rule, not just the four store_memory write values. The same filter is available over the Connect read API on ListMemories. |
created_after |
string | no | RFC3339 timestamp — include only records with created_at >= created_after (inclusive lower bound) |
created_before |
string | no | RFC3339 timestamp — include only records with created_at < created_before (exclusive upper bound). Half-open window: [created_after, created_before) |
cursor |
string | no | Opaque pagination cursor from a previous response’s next_cursor. Omit for the first page. Mutually exclusive with offset |
full |
bool | no | Return full content instead of compact summaries (default false) |
cross_spine |
bool | no | Span all scopes the caller can read; ignores scope when true |
Returns { "memories": [...], "next_cursor": "<token>" }. An empty or absent next_cursor indicates the last page.
citations are omitted from the default compact view; pass full=true to include them.
The result is returned as structured content and, per MCP 2026-07-28, also as
the same JSON in a text block.
On a cross-spine call (cross_spine=true), the response also carries
searched_scopes — every scope you can read that the list spanned, not the
scopes that produced results — and scopes_truncated, true when that scope
enumeration hit its bounded ceiling and the list may be incomplete. Both keys
are omitted entirely on a scope-confined call.
If the coverage enumeration itself fails after results were already found,
the call still succeeds: scopes_unknown is true, searched_scopes is
absent (never an empty list, which would read as “searched nothing”), and
scopes_truncated is absent/false.
The response also carries recall_gate_hidden — {total, archived, superseded, expired, scheduled} counts of records the recall gate hid from
this same page (same limit, cursor/offset, and every filter), with the
gate lifted, counting only states this call did not already include. Present
on a scope-confined call and a cross-spine call alike. Absent when the count
could not be computed; all-zero when nothing was hidden. A record hidden for
more than one reason counts once in total and once per state it carries,
so the per-state fields can sum to more than total. There is no argument
to include hidden records in this page — fetch a specific one by id with
get_memory.
Pass an explicit limit on a cross-spine list. The underlying total becomes
an exact count across every readable scope rather than one scope (visible as
the Connect API’s total field), and on the Connect lane an unset limit
resolves to the maximum, 1000 — pass an explicit limit and page the remainder
by offset or page_token rather than relying on the default.
list_scheduled
Section titled “list_scheduled”List your windowed memories the recall gate is hiding. Active windowed records
surface via list_memory/search_memory, not here. ListScheduled stays
owner-only even when cross_spine spans every scope — another actor’s shared
scheduled or expired record never appears here (deferred reveal).
| Argument | Type | Required | Description |
|---|---|---|---|
scope |
string | required unless cross_spine |
The scope to list scheduled/expired memories from |
state |
string | no | scheduled (default, not yet active), expired, or all |
limit |
uint64 | no | Maximum memories to return; 0 resolves to this tool’s default, 20; values above 1000 (the maximum) are rejected (field=limit hint=out_of_range) |
created_after |
string | no | RFC3339 timestamp — include only records with created_at >= created_after (inclusive lower bound) |
created_before |
string | no | RFC3339 timestamp — include only records with created_at < created_before (exclusive upper bound). Half-open window: [created_after, created_before) |
cross_spine |
bool | no | List across every scope the caller can read (still only the caller’s own records; ignores scope if supplied) |
cursor |
string | no | Opaque pagination cursor from a prior next_cursor; omit for the first page |
Returns { "memories": [...], "next_cursor": "..." }, the matching hidden
windowed records and an opaque token for the next page (empty when this is
the last page). A cross_spine call additionally carries searched_scopes/
scopes_truncated (or scopes_unknown) — see list_memory above.
The result is returned as structured content and, per MCP 2026-07-28, also as
the same JSON in a text block.
get_memory
Section titled “get_memory”Fetch one memory by id.
| Argument | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The UUID or short_id of the memory to fetch |
Returns the full memory record. Authenticated callers can read their own records
plus any shared records. Anonymous callers can only read ownerless records.
Fetch-by-id is not recall-gated: it returns every state search_memory/
list_memory/search_discovery/list_scheduled hide, in the same order every
other surface emits them (descending by finality) —
- archived — explicitly retired via the
engram spine-review archiveCLI command (reversible viarestore); seereference/memory-recordfor thearchived_atfield’s contract - superseded — corrected away by
supersede_memory; see that section for the full soft-hide contract - expired — the validity window has ended:
not_afteris at or before now. The upper bound is exclusive, so a record whosenot_afterequals the current instant is already expired — see the validity window for the full boundary rule - scheduled — not yet active:
not_beforeis in the future. The lower bound is inclusive, so the two ends of the window do not behave symmetrically — see the validity window
get_memory also returns schema_version on every fetch — not a soft-hidden
state, just a value every fetch carries. See
Schema version for the full
forward-compatibility contract.
get_memory always returns citations in full — unlike search_memory/
list_memory, it has no compact view to omit them from.
supersede_memory
Section titled “supersede_memory”Correct one or more memories you own by superseding them: stores a single new,
correcting record and marks each target superseded_by that new record.
Correction is explicit and preserves history — nothing is deleted or overwritten.
A merge may span different scopes, categories, and visibilities; there is no
requirement that targets be alike.
Takes the full store_memory field set for the new, correcting record, plus:
| Argument | Type | Required | Description |
|---|---|---|---|
supersedes |
array of string | yes | One or more ids — each a full UUID or a short_id — of the memories this new record corrects. A one-element array is the ordinary single-target case. |
validate_only |
bool | no | Run the full preflight (ownership, single-live-head, rule rejection, ambiguous short_id) without writing anything; returns the resolved targets, or the exact rejection a real call would produce. Optional — useful before a multi-target merge, not a routine extra round trip. Never consults idempotency_key. |
There is no maximum target count — the set is unbounded. Duplicate targets — the
same id given twice, or two spellings of the same record (a short id and its
matching UUID) — collapse to one target; you do not need to dedupe your input
before calling. Each individual entry is bounded at 256 bytes — generous for a
real UUID or short_id, which never approaches that length — and an
oversized entry rejects the call before any target is resolved; this bounds
the LENGTH of one entry only and does not reintroduce a cap on how many
targets the set may contain.
Everything else (content, scope, category, tags, summary, citations,
repo/workspace/worktree/base_dir, source) describes the new record and behaves
exactly as in store_memory, including citations — an
optional array of structured source anchors, never inferred.
Idempotency. idempotency_key is accepted on this verb — the replay
fingerprint covers the new record’s content and the target set together, so the
same idempotency_key against a different target set is a conflict, not a
replay. A retry with the same key and the same target set — in any order, with
any duplicates — returns the original result instead of merging again. Because
the replay check runs after target existence and ownership are checked, a retry
whose targets were deleted, or whose ownership changed, since the first call
does not replay — it is rejected as not found, the same as any other
addressability failure.
What changes. The new record is stored normally and carries a supersedes
link to every target; each target gains a superseded_by link back to the new
record. All of these are additive payload links — no target’s content, tags, or
vector is touched.
Recall behavior. A superseded record is soft-hidden from search_memory,
list_memory, search_discovery, and list_scheduled, so recall returns only the
current truth. It stays fully fetchable by id via get_memory,
which is not recall-gated — so the superseded history remains auditable. This is
one of the four states get_memory’s own section lists as recall-hidden-but-fetchable
(scheduled, expired, superseded, archived); archived is a separate, independently
maintained state — see get_memory and
reference/memory-record — never entered or
cleared by this verb.
If the merge fails partway through. When stamping superseded_by onto every
target fails partway through, the server compensates by removing the new record
and clearing whatever links it managed to leave behind, so a failed merge is
usually observably a no-op. Compensation talks to the same store that just
failed, though, so when it cannot complete, the call can leave an orphaned new
record, one or more targets still carrying a link to it, or both — there is no
retry queue and no recovery command; the server logs the affected ids for the
operator, and that log is the only remediation path. Once a merge attempt has
returned and its cleanup succeeded, no partial state remains — but that is a
statement about the state after the call, not about every instant during it: a
concurrent, lock-free read can briefly see a target hidden while the merge is
still in flight.
Constraints.
- Owner-only. Routes through the ownership write gate, per target. A
sharedrecord you can read is not one you can supersede; a target you do not own, a target that does not exist, and a target whose short id is ambiguous (matches more than one record) are all the same rejection — indistinguishable from each other — and the response names every offending target of the set, not just the first. - Single live head. Superseding an already-superseded record is rejected
(Connect:
failed_precondition). This rule applies per target: naming even one already-superseded id in the set rejects the whole call, naming every such target. Always target the current head — forward chains (C supersedes B supersedes A) are how history accumulates, and this makes cycles and self-supersession structurally impossible. - Never automatic. No similarity threshold or write-through path ever supersedes a record; it is only ever this explicit call.
- Rules are immutable. A
store_rulerecord cannot be superseded — naming one anywhere in the target set rejects the whole call, naming every rule target — delete the rule instead (same restriction asset_visibility).
Returns the new record’s id and short_id. With validate_only=true,
returns { validated: true, supersedes: [...], targets: [...] } instead —
the resolved target ids and no id/short_id, because nothing was written —
or the same rejection a real call over the same inputs would produce.
update_memory
Section titled “update_memory”Replace a memory’s content in place. The content is re-embedded. Optionally
toggle visibility, replace the tag set, or update the summary. The record’s id,
created_at, and ownership are preserved across the update. Important: if
the record has a caller-authored summary (summary_source=client), you must
address it when changing content — re-send it (unchanged), update it (revised
summary), or clear it (empty summary) — or the update is rejected.
| Argument | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The UUID or short_id of the memory to update |
content |
string | yes | The replacement text (re-embedded). Max ENGRAM_MEMORY_MAX_CONTENT_BYTES bytes (default 65536; see Configuration); enforced when the content changes. |
shared |
bool | no | true = shared, false = private; omit to keep current visibility |
tags |
string[] | no | Replaces the full tag set; an empty array clears all tags. Omit to keep the current tags. At most ENGRAM_MEMORY_MAX_TAGS tags (default 128) of at most ENGRAM_MEMORY_MAX_TAG_BYTES bytes each (default 128); enforced when the tag set changes. |
summary |
string | no | Replace the summary; empty string clears it. Omit to keep the current summary. When changing content, must be addressed if summary_source=client. Max ENGRAM_MEMORY_MAX_SUMMARY_BYTES bytes (default 512). |
Only the record owner can update. Returns "updated" on success.
delete_memory
Section titled “delete_memory”Delete one memory by id.
| Argument | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The UUID or short_id of the memory to delete |
Only the record owner can delete. Returns "deleted" on success.
Deleting a rule is permitted — this is the only path that retires one, since
supersede_memory and un-sharing are both rejected for rules — and no
server-side guard here distinguishes a rule from any other record. Agents
following the curating-memory skill are instructed to propose a rule’s
removal and never perform it unasked; that is an instruction-level gate, not
an enforced one.
delete_all
Section titled “delete_all”Delete your own memories in a scope (teardown). Never deletes another caller’s records.
| Argument | Type | Required | Description |
|---|---|---|---|
scope |
string | yes | The scope to clear |
Returns "scope cleared" on success.
store_discovery
Section titled “store_discovery”Cache agent-earned codebase understanding with source citations. Discoveries
live in a separate discovery:repo:* scope and are recalled on demand, never
at session start.
| Argument | Type | Required | Description |
|---|---|---|---|
content |
string | yes | The understanding to cache (embedded and searched); max 64 KiB |
kind |
string | yes | map (orientation/structure) or fact (pinned checkable claim) |
citations |
citation[] | yes | At least one source anchor (max 50) |
scope |
string | yes | Must start with discovery:, e.g. discovery:repo:my-repo |
tags |
string[] | no | Free-form labels |
summary |
string | no | Short human-readable summary |
id |
string | no | Omit to create; supply the UUID or short_id to replace in place |
Each citation object:
| Field | Type | Required | Description |
|---|---|---|---|
kind |
string | yes | file, commit, url, or repo |
ref |
string | yes | Path, repo URL, or doc URL |
locator |
string | no | E.g. 200-240 line range |
pin |
string | no | Commit SHA, content-hash, @rev, or fetched-at (aging anchor) |
excerpt |
string | no | Cached substance (max 16 KiB, soft cap ~50 lines) |
The source field is always agent-inferred and category is always
discovery — both are server-set; do not supply them.
Returns the stored discovery’s id and short_id.
search_discovery
Section titled “search_discovery”Semantic search over the discovery pool. Scope is required unless
cross_spine=true.
| Argument | Type | Required | Description |
|---|---|---|---|
query |
string | yes | Natural-language search query |
scope |
string | conditional | Discovery scope; required unless cross_spine is true |
kind |
string | no | map or fact filter |
k |
uint64 | no | Number of results to return; 0 resolves to this tool’s default, 8; values above 1000 (the maximum) are rejected (field=k hint=out_of_range) |
cross_spine |
bool | no | Span all discovery scopes; ignores scope when true |
Returns { "discoveries": [...] }. Results carry citations and created_at
(useful as aging signals). The result is returned as structured content and,
per MCP 2026-07-28, also as the same JSON in a text block.
With ENGRAM_SEARCH_RANKER=jev
enabled, results are instead reordered by the typed-decision provider’s
probability that each discovery answers the query — the base order is
discovery’s own vector-similarity order (discoveries have no lexical rank
step), then a stable sort by that probability — and each hit carries a
per-hit relevance value between 0 and 1 (values all near zero mean nothing
returned actually answers the query). Callers decide relevance for
themselves from these per-hit values; no hit is filtered out on the server’s
behalf, and there is no response-level flag. relevance is absent on every
hit when the ranker is off, and also absent (with the vector-similarity order
unchanged) when a rerank attempt fails — the search still succeeds, falling
back to the default order.
set_visibility
Section titled “set_visibility”Share or unshare a memory you own. Does not re-embed; only flips the visibility flag.
| Argument | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The UUID or short_id of the memory |
shared |
bool | yes | true = readable by any authenticated caller; false = private |
Only the record owner can change visibility. Sharing grants read, never write.
Returns "visibility updated" on success.
store_rule
Section titled “store_rule”Persist a normative rule (repository/project ground truth) that agents must
follow. An agent may notice a rule candidate and propose it to the user;
store_rule is called only after the user says yes — never promote a rule
unilaterally. See the curating-memory skill for the recognition triggers and
the proposal protocol. Rules live in a dedicated rule:repo:* /
rule:project:* scope, are always shared, and surface as a session-start
index.
| Argument | Type | Required | Description |
|---|---|---|---|
content |
string | yes | The full rule text (normative constraint); max 8 KiB |
scope |
string | yes | rule:repo:<repo> or rule:project:<project> |
summary |
string | yes | One-line index entry: a single physical line (no newlines), max 256 bytes |
tags |
string[] | no | Concern-area labels, e.g. vcs, deploy, authz |
id |
string | no | Omit to create; supply the UUID or short_id to replace in place |
category (rule), source (user-said), and visibility (shared) are all
server-set; do not supply them. The summary is stored as a client-authored
summary. A replace (id set to the existing UUID or short_id) preserves the
record’s existing short_id, so handles cited elsewhere keep resolving.
set_visibility is rejected for rules — they are always shared — and so is
supersede_memory; both correction paths are closed, so retiring a rule
means deleting it.
Returns the stored rule’s id and short_id.
list_rules
Section titled “list_rules”List the complete rule set for one or more rule:* scopes, up to 1000
rules per scope (the same documented recall maximum every other listing/search
tool shares), oldest-first; omit scopes to list every readable rule scope’s
rules in ONE cross-scope read, up to 1000 rules in total rather than per
scope. Rules are the repository/project’s normative ground truth.
| Argument | Type | Required | Description |
|---|---|---|---|
scopes |
string[] | no | One or more rule:* scopes to fetch the complete rule set from; omit for every readable rule scope’s rules |
tags |
string[] | no | Restrict to rules carrying all listed tags (AND) |
full |
bool | no | true adds full content; default returns the compact index shape |
The default compact shape is a ruleView (short_id, id, summary, tags,
scope, created_at) — note it carries no content, so a contradiction or
duplication check needs full=true. full=true returns the full records.
Ordering is oldest-first (this ascending order is specific to list_rules),
within each explicit scope and across the whole all-scopes read alike.
The result is returned as structured content and, per MCP 2026-07-28, also as
the same JSON in a text block.
A per-scope count above 50 adds a curation-smell advisory to the result under
advisory (absent otherwise) — the rules payload is unaffected. The
advisory is a volume signal only: it says nothing about duplication or
contradiction, and it cannot fire below 51 rules in a scope.
Omitting scopes additionally carries searched_scopes/scopes_truncated
(or scopes_unknown) naming ONLY the rule scopes covered — never a non-rule
scope the caller can also read — see list_memory above for the shared
three-state coverage semantics.
related_memories
Section titled “related_memories”Return one record’s neighbourhood: its supersession chain, records sharing a
citation, records sharing a rarity-weighted tag, and its nearest vector
neighbours — each edge carrying evidence typed to how it was found. Call this
only on demand: curating (dedup before a store, finding what a correction
should supersede) or an explicit user ask. Never call it at session start,
and never as an automatic follow-up to a search — the same on-demand framing
search_discovery already uses.
| Argument | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The UUID or short_id of the anchor memory |
k |
uint64 | no | Widens only the vector-neighbour cap; 0 resolves to this tool’s default, 8; values above 1000 (the maximum) are rejected (field=k hint=out_of_range) |
full |
bool | no | Return full content on the anchor and every neighbour instead of compact summaries (default false) |
Returns { "anchor": {...}, "related": [{ "memory": {...}, "edges": [...] }], "truncated": bool }.
Each edge is a flat object drawn only from {type, score, shared_tags, tag_weight, shared_citations, direction, depth} — type is one of
supersession, citation, tag, or vector; score (vector), shared_tags
tag_weight(tag),shared_citations(citation), anddirection+depth(supersession) are populated only for their own edge type. A record may appear once per edge type that connects it to the anchor.
Isolation is unconditional: another actor’s private record never appears,
even one sharing the anchor’s tag or citation; an anchor you cannot read
returns not_found echoing only your own input. The result is returned as
structured content and, per MCP 2026-07-28, also as the same JSON in a text
block.
list_tags
Section titled “list_tags”Return exact, recall-visible tag counts for a scope, or (scope omitted) every
scope the caller can read. Use it before store_memory to
reuse an existing tag rather than invent a near-duplicate, and to choose a
tags filter for search_memory/list_memory.
| Argument | Type | Required | Description |
|---|---|---|---|
scope |
string | no | Scope to count tags in; omit for every readable scope |
limit |
uint64 | no | Maximum distinct tags to return; 0 resolves to this tool’s default, 100; values above 1000 (the maximum) are rejected (field=limit hint=out_of_range) |
Returns { "tags": [{ "tag": "...", "count": N }], "more": bool }, sorted by
count descending. Counts cover recall-visible records only — an archived,
superseded, expired, or scheduled record’s tags are not counted. There is
no server-side prefix filter: more: true means the top-N list is
truncated, not that no more tags exist; filter the returned list yourself if
you need a narrower match. The result is returned as structured content and,
per MCP 2026-07-28, also as the same JSON in a text block.
archive_memory
Section titled “archive_memory”Retire one or more memories you own, reversibly, without deleting them.
Archived records drop out of search_memory / list_memory /
search_discovery / list_scheduled but stay fetchable via
get_memory. Nothing is deleted, and no other derived state
(superseded_by, not_before/not_after) is touched — see
Archiving for the full
independently-cleared-state contract.
Discriminate against its siblings: delete_memory removes
junk with no history worth keeping; supersede_memory
records a reversal because the fact itself changed; archive_memory retires
a record that is still true but no longer useful. Use only after the user
has explicitly agreed to it in the conversation — never as automatic
tidy-up.
| Argument | Type | Required | Description |
|---|---|---|---|
ids |
string[] | yes | 1 to 1000 ids, each a full UUID or short_id. Each entry is bounded at 256 bytes. |
Returns one outcome per id, in the order you supplied them:
archived, already_archived, or not_found. A duplicate id in the list is
reported once per occurrence, never merged or deduplicated. id is empty on
a not_found row and set to the resolved UUID otherwise.
A target you do not own and a target that does not exist both read
not_found — the same indistinguishable-by-design rejection
supersede_memory uses for its target set — so the
call never echoes a UUID for either case. The whole call rejects only on a
malformed batch (empty ids, a blank entry, more than 1000 entries, or an
entry over 256 bytes); see
Batch outcomes.
Operators reach the identical effect via engram spine-review archive on
the CLI.
restore_memory
Section titled “restore_memory”Reverse an archive_memory call: clears archived_at,
returning the record to normal recall. Never a delete, content erasure, or
vector removal.
| Argument | Type | Required | Description |
|---|---|---|---|
ids |
string[] | yes | 1 to 1000 ids, each a full UUID or short_id. Each entry is bounded at 256 bytes. |
Returns one outcome per id, in the order you supplied them: restored
(the record was archived and is now not), not_archived (it was not
archived — nothing to restore), or not_found — the same
indistinguishable-by-design rejection as archive_memory above. A duplicate
id in the list is reported once per occurrence, never merged. id is empty
on a not_found row and set to the resolved UUID otherwise. Rejection
conditions and the malformed-batch envelope are identical to
archive_memory.
Operators reach the identical effect via engram spine-review restore on
the CLI.
CLI: summarize-missing
Section titled “CLI: summarize-missing”Fill summaries for memories that do not have one (summary_source=auto).
Auto-generated summaries are created offline using the configured model.
engram summarize-missing (--scope <scope> | --all-scopes) [flags]Like the other sweep-style operator commands (spine-review scan, spine-review verify, spine-review consolidate), this command enforces one constraint: a sweep requires an explicit –scope or –all-scopes: name one scope, or opt into every scope.
| Flag | Type | Default | Description |
|---|---|---|---|
--scope |
string | "" |
Only summarize records in this scope |
--all-scopes |
bool | false |
Sweep every scope (required if --scope is omitted) |
--older-than |
duration | 0 |
Only records created at least this long ago (0 = any age) |
--limit |
int | 0 |
Max records to scan (0 = no cap) |
--dry-run |
bool | false |
Count eligible records without writing |
--timeout |
duration | 30m |
Max wall-clock for the sweep (0 disables); also cancellable via Ctrl-C |
Requires ENGRAM_SUMMARY_MODEL environment variable (e.g. gpt-4o-mini); the command errors if it is unset.
Creates summaries with summary_source=auto and stores them back in place.
Respects ENGRAM_SUMMARY_MAX_CHARS (default 280) for summary length.