Skip to content

Willder documentation

MCP memory tools reference.

Choose a tool, supply its arguments and check the result. The examples below show the params object for a tools/call request.

Read tools require read permission or higher. Write tools require write permission or higher. Scope and token validity are checked when a tool runs. Examples with placeholder IDs need real, authorized IDs before use. Write examples change data.

memory_query

Find facts relevant to a question. Requires read.

query (required)
String. A focused question in plain language.
limit (optional)
Optional integer from 1 to 50. Defaults to 10.
scopePath (optional)
Optional string. Copy an existing scope path from Access. Defaults to the token scope.
ifRevision (optional)
Optional string. The revision from your last identical call. If the answer is unchanged you get a short receipt instead of the answer again.
JSON
{
  "name": "memory_query",
  "arguments": {
    "query": "What is the agreed launch date for Project Atlas?",
    "limit": 5
  }
}

Result: Text bullets, one line per fact, each naming the connected source or actor it came from (‘— uncited’ when the fact carries neither) and, when available, its validity date, then a ‘revision:’ line. An empty result says ‘No facts found.’ When the question names a specific value (a number of three or more digits, quoted text, or a name of two or more capitalized words) that no memory you can read holds, a fixed ‘Note: no memory you can read holds…’ line follows the facts; it never repeats the value, and the result's _meta ‘willder/unheldValues’ lists those values by their position in the question. Sending that revision back as ifRevision returns ‘Unchanged since revision …’ instead of the same answer. A bullet that answers from one stored version ends with ‘· version <id>’: pass that id as memory_ingest's baseVersion to update the fact. A bullet a connected source synced from one item names it with ‘· item <id>’, before any version: the id that item's change notices name. This tool returns no graph fact UUIDs.

memory_entity

Read a known entity and its facts. Requires read.

entityId (required)
String. The ID of an existing entity you are authorized to access.
scopePath (optional)
Optional string. Copy an existing scope path from Access. Defaults to the token scope.
ifRevision (optional)
Optional string. The revision from your last identical call. If the answer is unchanged you get a short receipt instead of the answer again.
JSON
{
  "name": "memory_entity",
  "arguments": {
    "entityId": "replace_with_entity_id"
  }
}

Result: Text with the entity name, type, optional summary and facts, then a ‘revision:’ line, or ‘Entity not found.’ Sending that revision back as ifRevision returns ‘Unchanged since revision …’ instead of the same answer. The tool does not search for IDs by name.

memory_version

Read one stored version of a fact or rule by id. Requires read.

versionId (required)
Nonempty string after trimming. The version's id, as a change notice reported it.
JSON
{
  "name": "memory_version",
  "arguments": {
    "versionId": "replace_with_version_id"
  }
}

Result: Text naming the version, whether it is current, superseded or retired, the scope it lives in, who wrote it and when, the version that superseded it and the versions it superseded, then its own text. ‘Version not found.’ when this org has no such version or it lives in a scope you cannot read; the two answers are deliberately alike.

memory_changes

List memory changes other agents and people made since you last looked. Requires read.

limit (optional)
Optional integer, 1 to 50. Max changes to return (default 20).
scopePath (optional)
Optional string. Copy an existing scope path from Access. Defaults to the token scope.
JSON
{
  "name": "memory_changes",
  "arguments": {}
}

Result: One line per change, oldest first: what happened, the record, who did it, and a short summary of the record's title. ‘No changes.’ when nothing is new. Only changes inside your token's scope are listed; changes elsewhere are not counted or hinted at. There is no cursor to pass: the server holds your position and advances it before it answers, so calls you make one after another never return the same change twice. The ‘more changes waiting’ line appears only when a change you may read really does exist past this page.

memory_note

Tell other agents in your scope what you are working on right now. Requires write.

kind (required)
One of: claim (I am doing this), intent (about to try this), status, question.
task (optional)
Optional string, at most 512 characters. What the note is about. Two claims on the same task, compared without regard to case, collide. Defaults to the task the session names in _meta["willder/task"]; a post that names neither is refused.
body (optional)
Optional string, at most 280 characters. One line saying what you are doing. Required to post, except a claim that carries touches; ignored on release.
expiresInMinutes (optional)
Optional integer, 1 to 240. How long the note stays live (default 30). Posting the same kind on the same task again renews it from now.
action (optional)
Optional. ‘post’ (the default) or ‘release’, which ends your live note of this kind on this task.
touches (optional)
Optional, a claim only: the files you are editing, as "file:<repo>/<path>", at most 64. Sent with a body they replace the claim's earlier list; sent without one, as the hook sends them, they are added to it. A touch that is not a file node is left out and counted. Paths only. Recorded only where the org has claim overlap turned on. The Claude Code hook sends them for you when WILLDER_CLAIM_TOUCHES=on.
scopePath (optional)
Optional string. Copy an existing scope path from Access. Defaults to the token scope.
JSON
{
  "name": "memory_note",
  "arguments": {
    "kind": "claim",
    "task": "kickback on the ingest retry fix",
    "body": "Reproducing the duplicate write before changing anything.",
    "expiresInMinutes": 60
  }
}

Result: ‘Posted claim <id> on "<task>" in <scope>, live until <time>.’, or ‘Renewed …’ for a note you already had live. When another agent already holds a live claim on the same task in a scope you can read, the reply adds ‘Detected 1 earlier live claim on this task…’ and names that agent, when it claimed, until when and its line. Both claims are kept; nothing decides between them. A claim with touches that shares a file with another agent's live claim on other work adds ‘Overlapping files detected with 1 other live claim…’, naming that claim and the shared files, and that agent hears of it on its next tool call when its own scope covers both claims (one in a sibling scope is not told); again nothing decides between them. After the org turns claim overlap off, overlaps already recorded keep showing until the claim that made them ends. Other agents see your note on their next tool call and at the start of their next session, until it expires or you release it. A note is not a fact: memory_query never returns it.

memory_ingest

Add a note to your scope's memory. Requires write.

content (required)
Nonempty string. The note or observation to store.
name (required)
Nonempty string. A name for the subject of the note.
scopePath (optional)
Optional string. Copy an existing scope path from Access. Defaults to the token scope.
kind (optional)
Optional. ‘convention’ (how your org does something) or ‘decision’ (a choice later work must follow). Records the note as a rule of the scope, served at the start of every session there. A decision an agent records is a proposal until a person confirms it from the link in the reply. Omit for an ordinary note.
supersedes (optional)
Optional string, with kind. The id of the rule this one replaces; the old rule stops being served. A rule can be replaced once, by someone who can write its scope. Naming a rule already replaced is refused and nothing is stored; memory_version on that id names the version that replaced it.
baseVersion (optional)
Optional string, without kind. The id of the version of this fact (same name, same scope) you read and are replacing, from memory_query's ‘· version’, memory_version or a change notice. The write replaces that version. When someone else replaced it since, the write is refused with ‘stale_version:’ naming the current version: read it, then write again with that id. A base that is not a version of this fact is refused with ‘unknown_version:’. Omit to add a version that replaces nothing: it is still refused with ‘stale_version:’ when the fact has a version you were never served, stored after the newest one you read or wrote and more than a second before this write; read that version, then write again. A version stored within that second is a concurrent write: the conflict rule settles it, and a losing write is stored and told which version won.
role (optional)
Optional, without kind. What this note is to the task your session names: ‘cause’ (why the task exists or what went wrong), ‘constraint’ (a limit the work must stay inside), ‘decision’ (a choice later work must follow), ‘rejected’ (an option considered and turned down) or ‘progress’ (what was done or where the work stands). The next session of the same task is served its cause, constraint, decision and rejected notes first, then notes with no role, then progress; within each, those most relevant to what the session is doing first, the newest first among equals. Set it whenever you know it. Sent with kind, the write is refused and nothing is stored.
occurredAt (optional)
Optional string. When the content happened, if not now: an ISO-8601 date (2026-03-11, read as midnight UTC) or date-time with an offset (2026-03-11T15:10:00Z). memory_query shows this date on the note's bullet and, between versions of one note (same name, same scope), serves the newest first. A value without an offset, a day the calendar does not have, or a time more than five minutes ahead of the server's clock is refused with ‘invalid_input:’ and nothing is stored; a date is never recorded later than when the note is stored. It dates and orders the note only: graph extraction keeps the server's clock. When the same text is already stored, nothing is written; if its date differs from the one this write would have had, the reply names the version and the date that stands. Omit it and the note is dated by when it was stored.
intentionalShareFrom (optional)
Optional list of up to 20 scope paths. The scopes whose content this write shares on purpose. Willder records which scopes each agent's session was shown content from; a write into a scope after the session read a scope outside it (a sibling, or a parent) is refused with ‘derived_write:’, nothing is stored, and the refusal names the scopes to declare. Send them here to share anyway: the note is stored, a ‘Shared content from a restricted scope’ row is written to the source scope's audit log, the note is marked ‘shared from a restricted scope’ for readers who cannot read the source (readers who can see its name), and the source scope's readers are told on their change feed. A declared scope covers what the session read inside it. Naming a scope the session did not read, whether or not it exists or you may read it, or one inside the target, is refused. A session is your agent's run of memory calls across all its connections and tokens; it ends after 30 minutes with no memory read or write.
idempotencyKey (optional)
Optional string, 1 to 255 characters. A key you generate, unique to this write. Retrying the identical call with the same key writes nothing and returns the first call's reply; the same key with any argument changed is refused; a retry while the first call is still writing is refused with ‘still in progress’, so retry again shortly. Keys belong to your agent in your org and do not expire.
JSON
{
  "name": "memory_ingest",
  "arguments": {
    "name": "Project Atlas",
    "content": "Example only: the team agreed to launch Project Atlas on 15 October 2026.",
    "idempotencyKey": "replace_with_idempotency_key"
  }
}

Result: ‘Stored. Queued for ingest — will appear in memory shortly.’ This confirms queue acceptance. It does not confirm that extraction finished or that the note is searchable. To retry a call whose reply you lost, send it again with the same idempotencyKey. When the note is readable at once, the reply says so and ends with ‘Version: <id>’, the id a later update passes as baseVersion. With kind, the reply starts with the new rule's id, which is what a later supersedes names.

get_my_brief

Read a generated brief. Requires read.

briefId (optional)
Optional string. The ID of an existing brief.
scopePath (optional)
Optional string. Copy an existing scope path from Access. Defaults to the token scope.
JSON
{
  "name": "get_my_brief",
  "arguments": {}
}

Result: Markdown containing the title, scope, summary and sections, with a recipient or Google Doc link when present. Without briefId, returns the latest brief at the selected scope, or ‘No brief found for this scope.’ With an ID, access is checked against the brief’s own scope.

memory_stale

What to refresh next: records whose cited or source fact has been replaced, or changed after they were built. Requires read.

scopePath (optional)
Optional string. Copy an existing scope path from Access. Defaults to the token scope.
limit (optional)
Optional integer, 1 to 20. Max records to return (default 3).
JSON
{
  "name": "memory_stale",
  "arguments": {}
}

Result: One line per record, most depended-on first: the record ID, the fact it cites or rests on, whether that fact was replaced (and by what) or updated, superseded, deleted or revoked, when, and how many records rest on it. ‘Nothing stale in <scope>.’ when there is none. A last line says so when a query reached its 500-row bound and older records were not checked. Only what was recorded is read: a brief whose cited fact changed text without a recorded change, or whose connected source re-synced, is not listed; GET /api/briefs/:id/staleness checks one brief fully.

contact_read

Fetch a single contact by id. Requires read.

contactId (required)
String. The contact's id.
JSON
{
  "name": "contact_read",
  "arguments": {
    "contactId": "replace_with_contact_id"
  }
}

Result: Text with the contact's name, title, type, revision, and a SUPPRESSED warning when set, or ‘Contact not found.’

contact_upsert

Create a new contact or update an existing one. Requires write.

idempotencyKey (required)
Nonempty string. A caller-generated key unique to this logical write; reusing it retries safely.
contactId (optional)
Optional string. Update this exact contact. Omit to match/create by email.
baseRevision (optional)
Optional integer. Required when updating an existing contact — its current revision, from contact_search/contact_read. Omit when creating.
name (optional)
Optional string. Required when creating a new contact.
email (optional)
Optional string.
title (optional)
Optional string.
type (optional)
Optional. One of prospect, customer, investor, partner, advisor, vendor, team, other (default: prospect).
description (optional)
Optional string.
JSON
{
  "name": "contact_upsert",
  "arguments": {
    "idempotencyKey": "replace_with_idempotency_key",
    "name": "Jane Prospect",
    "email": "jane@example.com",
    "type": "prospect"
  }
}

Result: Text confirming ‘created’ or ‘updated’ with the contact's id, name and new revision. A suppressed contact's suppression can never be changed through this tool. A stale baseRevision is rejected rather than silently overwritten.

send_feedback

Send a bug report, a request or praise privately to the people who run this server. Requires read.

kind (required)
One of bug, request, praise, other.
message (required)
Nonempty string, at most 4,000 characters. The feedback, in the user's words where possible.
context (optional)
Optional string, at most 2,000 characters. What the user was trying to do when this came up.
JSON
{
  "name": "send_feedback",
  "arguments": {
    "kind": "bug",
    "message": "memory_query returned nothing for a fact I wrote a minute ago.",
    "context": "Checking that a write from one agent reaches another."
  }
}

Result: Text confirming it was sent, with a reference id. It reads and writes no memory and is never shown to another workspace. An agent that already sent 20 items in the last hour is refused until the hour passes.

Response format and limits

Tool results contain content text blocks. Tool failures set isError: true; protocol errors use the JSON RPC error envelope. Do not treat HTTP 200 alone as success.

After a tool's own answer, one more text block may follow. On the first call of a session it starts ‘Before you start’ and lists the rules recorded for your scope, what earlier sessions of your task decided and wrote (notes written with a role of cause, constraint, decision or rejected first, progress last), recent writes that mention your task, and facts flagged stale, in about 600 tokens at most. Name the session and the task with _meta on the call, {"willder/session":"run-42","willder/task":"what this run is for"}; without a session name, a client that opens a new MCP connection (it sends initialize and then returns the Mcp-Session-Id it was given, as Claude Code and the MCP SDKs do) starts a new session, and otherwise a pause of 30 minutes does. Any call may also list what others changed in your scope since your last call.

Read tools that accept a scope fall back to the token scope if a supplied scope path is malformed. Memory writes reject malformed nonempty paths. Always copy an existing path and validate the intended scope in your client.

There is no general MCP tool for editing or deleting extracted facts, issuing or revoking tokens, or sending email. Use memory_ingest with kind and supersedes to replace a recorded convention or decision you may write. Linking a source does not synchronize or update its fact.