MCP tools reference

The 50tools the Field Memo MCP server exposes to connected agents. Each tool's full description is what the agent itself sees. Tools marked sign-in required need an OAuth-authenticated session; the rest also work against a memo's edit/comment/read key.

create_memo

Create a new Field Memo. Returns role URLs (edit, comment, read). When called in an anonymous session the memo expires in 30 days unless claimed. If the response's attached_to_account is false, you MUST tell the user explicitly that the memo was 'saved anonymously and isn't on your Field Memo account' BEFORE presenting the URLs (do not soften this — the user often thinks they're signed in). Then ask, in a short friendly tone: 'Want to save this to your account? Signing up is free.' If the user replies yes, call claim_memo with this memo's edit URL — your MCP client will automatically prompt the user to sign in (one browser hop) and then retry the claim. If attached_to_account is true, do not ask about claiming — the memo is already on the user's account. TIP for nicer link previews: the memo's markdown may begin with a YAML front-matter block (---\ntitle: My Doc\ndescription: One-liner\n---). If present, title (case-insensitive) becomes the page <title> and the Slack/social link-unfurl headline; description becomes the unfurl blurb. Without front-matter, previews fall back to a generic 'A Field Memo' card (privacy: we never auto-extract H1 text into metadata). Recommend adding front-matter when a memo is intended for sharing. TABLE OF CONTENTS URL HINT: any returned memo URL accepts an optional toc=shown or toc=hidden query param (e.g. https://my.fieldmemo.io/m/abc123?k=…&toc=shown). When present it forces the per-memo TOC sidebar open or closed for that visitor; absent, it falls back to their saved preference. Append &toc=shown when handing a URL to the user for a long memo with many headings, or when they explicitly ask for a deeplink with the TOC open.

markdown stringtitle stringcollections array

get_memo

Fetch a memo by URL (with ?k=...) or id + key. Returns markdown and metadata. The response includes a sections outline: one entry per heading with its canonical heading_path (plus level, duplicate_occurrence, and ambiguous). When you want to target a section with read_section, replace_section, or append_to_section, COPY THAT heading_path VERBATIM instead of reconstructing it from the raw markdown. The stored markdown can contain escape characters the matcher does not expect (e.g. ### 13\. Confirmation renders as 13. Confirmation), so the canonical path is the rendered text. If ambiguous is true, two sections share the same path and the matcher will reject it; disambiguate with the user or edit one heading first. When relaying a memo URL back to the user, you may append &toc=shown (or &toc=hidden) to force the table-of-contents sidebar open or closed for the visitor — useful for long memos with many headings.

url stringid stringkey string

update_memo

Replace the full markdown of a memo. Requires an edit URL. PREFER a surgical tool over update_memo whenever the change fits one. Whole-memo replacement is expensive in tokens, races harder against concurrent editing, and is the most likely path to orphan comment threads. Reach for it only when the user actually asked for a rewrite of the whole thing: - Editing a single section → replace_section / append_to_section - Known string change → patch_memo (unified diff) - Adding to the end → append_to_memo - Toggling a checkbox → toggle_task - Adding a list item → add_list_item - Adding a table row → add_table_row - Setting metadata → set_field / rename_field / delete_field IMPORTANT: if you call get_memo before writing (or have called it recently), pass its returned version as base_version. The server will reject the write with a conflict error if another writer (browser or MCP) has modified the memo since. The rejection includes severity (minor if only 1–2 versions behind and same author, major if 3+ behind OR another author wrote since): on minor, TREAT current_markdown as ground truth, discard your draft, re-apply your intent, retry with the new base_version. On major, also consider surfacing the change to the user before overwriting — the world moved a real amount. If you truly mean to overwrite a major conflict, pass confirm_overwrite_changes: <current_version> (must equal the current_version from the prior rejection; if more concurrent writes land between attempts the flag stops working and you must re-confirm against the new number). Omit base_version only if you explicitly mean to overwrite whatever is there. Front-matter convention: a leading YAML block (---\ntitle: …\ndescription: …\n---) drives the page title and Slack/social link-unfurl previews. title and description are case-insensitive. Add or update them when a memo is intended for sharing — they're the only fields that surface to unfurlers. COMMENT THREADS: the response may include comments_orphaned (an array of thread IDs whose anchored passage you rewrote so that the in-document highlight could not be reattached) and comments_reattached (threads whose anchor text was preserved and re-marked automatically). Whenever a write touches any anchored thread, BOTH fields are returned (even empty), so comments_orphaned: [] is an explicit guarantee you can assert, not a missing field you have to infer from. When comments_orphaned is non-empty, surface that to the user — those threads still exist and are readable, but no longer point at a specific passage. The structure-aware tools (replace_section, patch_memo, etc.) report the same fields. CONCURRENT ACTIVITY: the response may include concurrent_activity: { last_other_writer, seconds_ago } when SOMEONE ELSE edited the memo in the last ~30s — another agent, or a person typing in the browser. Someone merely opening or viewing the memo (without editing) is NOT concurrent activity and never appears here. Purely advisory — no behaviour change — but when it fires, treat it as a hint to pause or confirm with the user before the next push, since you may be racing a live editor. ORPHAN-COMMENT BUDGET (TRA-337): writes that would orphan more than 3 comment threads are rejected with error: would_orphan_comments and a threads_at_risk[] payload that includes per-thread anchor context + comment body. Two ways forward: 1. PREFERRED — comment_anchors: [{ thread_id, new_anchor_text }]: find where each at-risk passage now lives in your rewritten markdown and re-pin it. Strict improvement; never adds orphans. 2. ESCAPE HATCH — confirm_orphan_count: <count>: acknowledge the loss. Use only after surfacing the orphan list to the user. The count must match the latest rejection; if new comments land in between, the count grows and your flag stops working. CONCURRENCY MODEL (how version and base_version relate): a memo carries a single integer version that get_memo returns. It increments by exactly 1 on every write that changes the memo's BODY or its front-matter FIELDS, from any surface: MCP and REST writes, AND a browser editor flushing typed changes (those land as a realtime-sourced bump). Comment, reaction, assignment, and collection-membership activity do NOT touch version (they are separate from the body), so a version that jumped between two reads means the body or fields actually changed in between, not that someone commented. base_version is OPTIONAL: pass the version you last read and the write is gated by optimistic concurrency, compare-and-swap against the current row, so it only lands if nothing changed since. If the memo moved, the write is REJECTED with a conflict (409-equivalent) carrying current_markdown, current_version, and a severity (minor = 1 or 2 versions behind, same author; major = 3+ behind OR a different author wrote since). OMIT base_version only when you explicitly intend a last-writer-wins overwrite of whatever is there now. There is no separate lock taken; the CAS is the whole mechanism, so two writes that both omit base_version simply apply in arrival order.

url stringid stringkey stringmarkdown string*base_version integerconfirm_overwrite_changes integercomment_anchors arrayconfirm_orphan_count integer

append_to_memo

Append markdown to the end of a memo. Requires an edit URL. PREFER omitting base_version for pure appends — they don't logically conflict with concurrent edits to other parts of the doc, and on a memo someone is actively typing in the version increments faster than you can read-then-write, leading to spurious conflict loops. Pass base_version only when you need the append to land atomically on top of a specific snapshot you just read — see update_memo for the conflict-and-retry pattern (and the confirm_overwrite_changes escape hatch) when that matters. Append is the lowest-risk write for comment threads — the existing body is left in place — but the TRA-337 orphan-budget gate still applies if the appended content somehow disturbs an existing anchor. CONCURRENCY MODEL (how version and base_version relate): a memo carries a single integer version that get_memo returns. It increments by exactly 1 on every write that changes the memo's BODY or its front-matter FIELDS, from any surface: MCP and REST writes, AND a browser editor flushing typed changes (those land as a realtime-sourced bump). Comment, reaction, assignment, and collection-membership activity do NOT touch version (they are separate from the body), so a version that jumped between two reads means the body or fields actually changed in between, not that someone commented. base_version is OPTIONAL: pass the version you last read and the write is gated by optimistic concurrency, compare-and-swap against the current row, so it only lands if nothing changed since. If the memo moved, the write is REJECTED with a conflict (409-equivalent) carrying current_markdown, current_version, and a severity (minor = 1 or 2 versions behind, same author; major = 3+ behind OR a different author wrote since). OMIT base_version only when you explicitly intend a last-writer-wins overwrite of whatever is there now. There is no separate lock taken; the CAS is the whole mechanism, so two writes that both omit base_version simply apply in arrival order.

url stringid stringkey stringmarkdown string*base_version integerconfirm_overwrite_changes integercomment_anchors arrayconfirm_orphan_count integer

list_my_memos

sign-in required

List memos owned by the authed user. Optionally filter by front-matter fields via where: an array of { field, equals } clauses (all must match). Requires OAuth.

limit numberwhere array

list_shared_with_me

sign-in required

List memos that have been shared with the authed user (i.e. memos they've opened that they don't own), most recently opened first. Requires OAuth. Returns the highest-level role each memo was opened with, last-opened time, and the role-specific URL the user can use to open it again. Useful when the user asks 'what did so-and-so share with me' or 'find that memo I read last week'.

limit number

delete_memo

Delete a memo. CLAIMED memos go to the user's trash for 30 days (recoverable from /dashboard/trash) — call again on the same id while it's already in trash to permanently delete now. UNCLAIMED memos are hard-deleted immediately. If the memo is unclaimed, an edit URL (or id + edit key) is enough. If the memo is claimed, you must be signed in via OAuth as the owner — an edit key alone is not sufficient. LOCKED memos cannot be deleted (TRA-345): the lock contract holds for destructive ops, so the owner must unlock_memo first. Soft-delete (to trash) and the second-call permanent delete both apply this guard.

id stringurl stringkey string

claim_memo

sign-in required

Attach an orphan memo to your account. Requires OAuth. Pass the memo's edit URL (or id + edit key).

url stringid stringkey string

share_memo

sign-in required

Email a memo's role-specific URL to one or more people. Requires OAuth and edit access to the memo. Use when the user asks for something like 'send the comment link to ben@pujji.co.nz with a note asking him to look at this before 5'. Notes should be short, friendly, in the user's voice — the recipient sees the note, the sender's name, and the sender's email. Optional toc arg: pass "shown" to append &toc=shown to the emailed URL so the recipient lands with the table-of-contents sidebar open (useful for long memos with many headings); omit otherwise. Caps: 10 targets per call, 30/memo/hour, 50/sender/day, 100/memo lifetime. Example call for that user request: `` share_memo({ url: "https://my.fieldmemo.io/m/abc123?k=...", role: "comment", targets: ["ben@pujji.co.nz"], note: "Could you have a look at this before 5?" }) ` Response shape: { ok, sent, rejected: [{email, reason}], rate_limit_remaining }. On 429, response includes rate_limited: { scope, retry_after_s } — relay the wait time to the user. Per-target failures (invalid_email, delivery_failed, self, duplicate) come back in rejected[]` with the call still 200; surface them concisely (e.g. 'sent to 2, couldn't reach foo@example.com — invalid email').

url stringid stringkey stringrole string*targets array*note stringtoc string

duplicate_memo

Create a full copy of an existing memo, including content, fields, images, embedded artifacts, and optionally comments. Any role (viewer, commenter, editor) can duplicate. Returns the new memo's role URLs. When called in an anonymous session the copy expires in 30 days unless claimed (same rules as create_memo — warn the user and offer claim_memo).

url stringid stringkey stringinclude_comments booleankeep_collections boolean

read_section

Read the markdown body of a specific section, selected by heading_path (a >-delimited path through nested headings, e.g. 'Meeting notes > Action items'). Returns just the section's content, not the whole memo. The response echoes the section's canonical heading_path (built from the rendered heading text, the exact form the section tools match on). COPY THAT VALUE VERBATIM when you follow up with replace_section / append_to_section, rather than reconstructing the path from the raw markdown, which may contain escape characters (e.g. ### 13\. Confirmation) that the matcher does not expect.

url stringid stringkey stringheading_path string*

replace_section

Replace the body of a specific section, keeping its heading. Pass heading_path verbatim from a get_memo sections entry or a read_section response (TRA-395) rather than reconstructing it from the raw markdown. Optimistically concurrency-checked via base_version (pass the version from get_memo). On conflict, see update_memo for the severity/confirm_overwrite_changes escape hatch — the same flag works here. TRA-337: also supports comment_anchors[] / confirm_orphan_count for the orphan-budget gate. CONCURRENCY MODEL (how version and base_version relate): a memo carries a single integer version that get_memo returns. It increments by exactly 1 on every write that changes the memo's BODY or its front-matter FIELDS, from any surface: MCP and REST writes, AND a browser editor flushing typed changes (those land as a realtime-sourced bump). Comment, reaction, assignment, and collection-membership activity do NOT touch version (they are separate from the body), so a version that jumped between two reads means the body or fields actually changed in between, not that someone commented. base_version is OPTIONAL: pass the version you last read and the write is gated by optimistic concurrency, compare-and-swap against the current row, so it only lands if nothing changed since. If the memo moved, the write is REJECTED with a conflict (409-equivalent) carrying current_markdown, current_version, and a severity (minor = 1 or 2 versions behind, same author; major = 3+ behind OR a different author wrote since). OMIT base_version only when you explicitly intend a last-writer-wins overwrite of whatever is there now. There is no separate lock taken; the CAS is the whole mechanism, so two writes that both omit base_version simply apply in arrival order.

url stringid stringkey stringheading_path string*markdown string*base_version integerconfirm_overwrite_changes integercomment_anchors arrayconfirm_orphan_count integer

rename_heading

Rename a heading in place, keeping its level and the body underneath it untouched. This is the surgical tool for changing heading TEXT — replace_section deliberately keeps the heading and rewrites the body, so reach for rename_heading when only the title line is wrong (a typo, a renamed person/section). Pass heading_path verbatim from a get_memo sections entry or a read_section response rather than reconstructing it. new_heading is the replacement text only (no leading # — the existing level is preserved); inline markdown like **bold** is honored. Optimistically concurrency-checked via base_version; on conflict see update_memo for the severity/confirm_overwrite_changes escape hatch. TRA-337: also supports comment_anchors[] / confirm_orphan_count for the orphan-budget gate, though renaming a heading rarely disturbs anchored passages in the body. CONCURRENCY MODEL (how version and base_version relate): a memo carries a single integer version that get_memo returns. It increments by exactly 1 on every write that changes the memo's BODY or its front-matter FIELDS, from any surface: MCP and REST writes, AND a browser editor flushing typed changes (those land as a realtime-sourced bump). Comment, reaction, assignment, and collection-membership activity do NOT touch version (they are separate from the body), so a version that jumped between two reads means the body or fields actually changed in between, not that someone commented. base_version is OPTIONAL: pass the version you last read and the write is gated by optimistic concurrency, compare-and-swap against the current row, so it only lands if nothing changed since. If the memo moved, the write is REJECTED with a conflict (409-equivalent) carrying current_markdown, current_version, and a severity (minor = 1 or 2 versions behind, same author; major = 3+ behind OR a different author wrote since). OMIT base_version only when you explicitly intend a last-writer-wins overwrite of whatever is there now. There is no separate lock taken; the CAS is the whole mechanism, so two writes that both omit base_version simply apply in arrival order.

url stringid stringkey stringheading_path string*new_heading string*base_version integerconfirm_overwrite_changes integercomment_anchors arrayconfirm_orphan_count integer

append_to_section

Append markdown at the end of a section (before the next sibling or parent heading). Ideal for 'add a bullet to Action Items' without replacing the whole section. Pass heading_path verbatim from a get_memo sections entry or a read_section response (TRA-395) rather than reconstructing it from the raw markdown. CONCURRENCY MODEL (how version and base_version relate): a memo carries a single integer version that get_memo returns. It increments by exactly 1 on every write that changes the memo's BODY or its front-matter FIELDS, from any surface: MCP and REST writes, AND a browser editor flushing typed changes (those land as a realtime-sourced bump). Comment, reaction, assignment, and collection-membership activity do NOT touch version (they are separate from the body), so a version that jumped between two reads means the body or fields actually changed in between, not that someone commented. base_version is OPTIONAL: pass the version you last read and the write is gated by optimistic concurrency, compare-and-swap against the current row, so it only lands if nothing changed since. If the memo moved, the write is REJECTED with a conflict (409-equivalent) carrying current_markdown, current_version, and a severity (minor = 1 or 2 versions behind, same author; major = 3+ behind OR a different author wrote since). OMIT base_version only when you explicitly intend a last-writer-wins overwrite of whatever is there now. There is no separate lock taken; the CAS is the whole mechanism, so two writes that both omit base_version simply apply in arrival order.

url stringid stringkey stringheading_path string*markdown string*base_version integerconfirm_overwrite_changes integercomment_anchors arrayconfirm_orphan_count integer

add_list_item

Append an item to a list in the memo. Use heading_path to target the first list in a specific section, or omit it and use list_index (0-based) to pick a top-level list. Set task: true (and optionally checked) to add a task-list item. CONCURRENCY MODEL (how version and base_version relate): a memo carries a single integer version that get_memo returns. It increments by exactly 1 on every write that changes the memo's BODY or its front-matter FIELDS, from any surface: MCP and REST writes, AND a browser editor flushing typed changes (those land as a realtime-sourced bump). Comment, reaction, assignment, and collection-membership activity do NOT touch version (they are separate from the body), so a version that jumped between two reads means the body or fields actually changed in between, not that someone commented. base_version is OPTIONAL: pass the version you last read and the write is gated by optimistic concurrency, compare-and-swap against the current row, so it only lands if nothing changed since. If the memo moved, the write is REJECTED with a conflict (409-equivalent) carrying current_markdown, current_version, and a severity (minor = 1 or 2 versions behind, same author; major = 3+ behind OR a different author wrote since). OMIT base_version only when you explicitly intend a last-writer-wins overwrite of whatever is there now. There is no separate lock taken; the CAS is the whole mechanism, so two writes that both omit base_version simply apply in arrival order.

url stringid stringkey stringheading_path stringlist_index integertext string*task booleanchecked booleanbase_version integerconfirm_overwrite_changes integercomment_anchors arrayconfirm_orphan_count integer

toggle_task

Toggle a task-list item's [ ] / [x] checkbox, or set it explicitly via checked. Locates the item by item_match (substring, case-insensitive; set fuzzy: true to allow looser matching). Errors with candidates if the match is ambiguous. CONCURRENCY MODEL (how version and base_version relate): a memo carries a single integer version that get_memo returns. It increments by exactly 1 on every write that changes the memo's BODY or its front-matter FIELDS, from any surface: MCP and REST writes, AND a browser editor flushing typed changes (those land as a realtime-sourced bump). Comment, reaction, assignment, and collection-membership activity do NOT touch version (they are separate from the body), so a version that jumped between two reads means the body or fields actually changed in between, not that someone commented. base_version is OPTIONAL: pass the version you last read and the write is gated by optimistic concurrency, compare-and-swap against the current row, so it only lands if nothing changed since. If the memo moved, the write is REJECTED with a conflict (409-equivalent) carrying current_markdown, current_version, and a severity (minor = 1 or 2 versions behind, same author; major = 3+ behind OR a different author wrote since). OMIT base_version only when you explicitly intend a last-writer-wins overwrite of whatever is there now. There is no separate lock taken; the CAS is the whole mechanism, so two writes that both omit base_version simply apply in arrival order.

url stringid stringkey stringheading_path stringitem_match string*fuzzy booleanchecked booleanbase_version integerconfirm_overwrite_changes integercomment_anchors arrayconfirm_orphan_count integer

remove_list_item

Remove a list item matched by item_match. Same matching rules as toggle_task. CONCURRENCY MODEL (how version and base_version relate): a memo carries a single integer version that get_memo returns. It increments by exactly 1 on every write that changes the memo's BODY or its front-matter FIELDS, from any surface: MCP and REST writes, AND a browser editor flushing typed changes (those land as a realtime-sourced bump). Comment, reaction, assignment, and collection-membership activity do NOT touch version (they are separate from the body), so a version that jumped between two reads means the body or fields actually changed in between, not that someone commented. base_version is OPTIONAL: pass the version you last read and the write is gated by optimistic concurrency, compare-and-swap against the current row, so it only lands if nothing changed since. If the memo moved, the write is REJECTED with a conflict (409-equivalent) carrying current_markdown, current_version, and a severity (minor = 1 or 2 versions behind, same author; major = 3+ behind OR a different author wrote since). OMIT base_version only when you explicitly intend a last-writer-wins overwrite of whatever is there now. There is no separate lock taken; the CAS is the whole mechanism, so two writes that both omit base_version simply apply in arrival order.

url stringid stringkey stringheading_path stringitem_match string*fuzzy booleanbase_version integerconfirm_overwrite_changes integercomment_anchors arrayconfirm_orphan_count integer

add_table_row

Append a row to a markdown table. Use heading_path to target the first table in a specific section, or omit it and use table_index (0-based) to pick a top-level table. Provide column values as either values (ordered array) or row (object keyed by header names, case-insensitive). Missing columns default to empty; extra columns are rejected with the expected headers. CONCURRENCY MODEL (how version and base_version relate): a memo carries a single integer version that get_memo returns. It increments by exactly 1 on every write that changes the memo's BODY or its front-matter FIELDS, from any surface: MCP and REST writes, AND a browser editor flushing typed changes (those land as a realtime-sourced bump). Comment, reaction, assignment, and collection-membership activity do NOT touch version (they are separate from the body), so a version that jumped between two reads means the body or fields actually changed in between, not that someone commented. base_version is OPTIONAL: pass the version you last read and the write is gated by optimistic concurrency, compare-and-swap against the current row, so it only lands if nothing changed since. If the memo moved, the write is REJECTED with a conflict (409-equivalent) carrying current_markdown, current_version, and a severity (minor = 1 or 2 versions behind, same author; major = 3+ behind OR a different author wrote since). OMIT base_version only when you explicitly intend a last-writer-wins overwrite of whatever is there now. There is no separate lock taken; the CAS is the whole mechanism, so two writes that both omit base_version simply apply in arrival order.

url stringid stringkey stringheading_path stringtable_index integervalues arrayrow objectbase_version integerconfirm_overwrite_changes integercomment_anchors arrayconfirm_orphan_count integer

list_fields

Return the memo's front-matter fields as a flat key/value map. Fields are structured metadata stored alongside the markdown body (not inside it). Use these tools to attach queryable metadata (status, tags, dates, etc.) to memos — list_my_memos can filter on them via the where clause.

url stringid stringkey string

get_field

Read a single front-matter field by key.

url stringid stringkey stringfield string*

set_field

Set a front-matter field. value accepts a string, number, boolean, null, or a simple array. Fields are stored in a separate DB column (not embedded in the markdown), so they survive round-trips cleanly and are queryable via list_my_memos where clauses. Note: a field write bumps the memo's version just like a body write. CONCURRENCY MODEL (how version and base_version relate): a memo carries a single integer version that get_memo returns. It increments by exactly 1 on every write that changes the memo's BODY or its front-matter FIELDS, from any surface: MCP and REST writes, AND a browser editor flushing typed changes (those land as a realtime-sourced bump). Comment, reaction, assignment, and collection-membership activity do NOT touch version (they are separate from the body), so a version that jumped between two reads means the body or fields actually changed in between, not that someone commented. base_version is OPTIONAL: pass the version you last read and the write is gated by optimistic concurrency, compare-and-swap against the current row, so it only lands if nothing changed since. If the memo moved, the write is REJECTED with a conflict (409-equivalent) carrying current_markdown, current_version, and a severity (minor = 1 or 2 versions behind, same author; major = 3+ behind OR a different author wrote since). OMIT base_version only when you explicitly intend a last-writer-wins overwrite of whatever is there now. There is no separate lock taken; the CAS is the whole mechanism, so two writes that both omit base_version simply apply in arrival order.

url stringid stringkey stringfield string*value anybase_version integerconfirm_overwrite_changes integer

rename_field

Rename a front-matter field key in place; the value is preserved and insertion order is maintained. Returns conflict if to already exists (decide whether to delete that key first). Useful when a memo started with one schema and the user wants to evolve it (e.g. statestatus). Note: title and description are reserved conventional keys that drive link-unfurl previews; renaming them away strips that signal from social/Slack previews. CONCURRENCY MODEL (how version and base_version relate): a memo carries a single integer version that get_memo returns. It increments by exactly 1 on every write that changes the memo's BODY or its front-matter FIELDS, from any surface: MCP and REST writes, AND a browser editor flushing typed changes (those land as a realtime-sourced bump). Comment, reaction, assignment, and collection-membership activity do NOT touch version (they are separate from the body), so a version that jumped between two reads means the body or fields actually changed in between, not that someone commented. base_version is OPTIONAL: pass the version you last read and the write is gated by optimistic concurrency, compare-and-swap against the current row, so it only lands if nothing changed since. If the memo moved, the write is REJECTED with a conflict (409-equivalent) carrying current_markdown, current_version, and a severity (minor = 1 or 2 versions behind, same author; major = 3+ behind OR a different author wrote since). OMIT base_version only when you explicitly intend a last-writer-wins overwrite of whatever is there now. There is no separate lock taken; the CAS is the whole mechanism, so two writes that both omit base_version simply apply in arrival order.

url stringid stringkey stringfrom string*to string*base_version integerconfirm_overwrite_changes integer

delete_field

Remove a front-matter field. Bumps the memo's version like any other field or body write. CONCURRENCY MODEL (how version and base_version relate): a memo carries a single integer version that get_memo returns. It increments by exactly 1 on every write that changes the memo's BODY or its front-matter FIELDS, from any surface: MCP and REST writes, AND a browser editor flushing typed changes (those land as a realtime-sourced bump). Comment, reaction, assignment, and collection-membership activity do NOT touch version (they are separate from the body), so a version that jumped between two reads means the body or fields actually changed in between, not that someone commented. base_version is OPTIONAL: pass the version you last read and the write is gated by optimistic concurrency, compare-and-swap against the current row, so it only lands if nothing changed since. If the memo moved, the write is REJECTED with a conflict (409-equivalent) carrying current_markdown, current_version, and a severity (minor = 1 or 2 versions behind, same author; major = 3+ behind OR a different author wrote since). OMIT base_version only when you explicitly intend a last-writer-wins overwrite of whatever is there now. There is no separate lock taken; the CAS is the whole mechanism, so two writes that both omit base_version simply apply in arrival order.

url stringid stringkey stringfield string*base_version integerconfirm_overwrite_changes integer

patch_memo

Apply a unified diff against the memo's current markdown. Useful for large memos where sending the full replacement would burn tokens. The diff must apply cleanly — fails loudly on any context/deletion mismatch (returns patch_conflict). Agent should then re-read with get_memo and regenerate the diff. Pass base_version to catch mid-air collisions; on major conflict see update_memo for the confirm_overwrite_changes escape hatch. TRA-337 orphan-budget rejection applies here too — same comment_anchors[] / confirm_orphan_count surface. CONCURRENCY MODEL (how version and base_version relate): a memo carries a single integer version that get_memo returns. It increments by exactly 1 on every write that changes the memo's BODY or its front-matter FIELDS, from any surface: MCP and REST writes, AND a browser editor flushing typed changes (those land as a realtime-sourced bump). Comment, reaction, assignment, and collection-membership activity do NOT touch version (they are separate from the body), so a version that jumped between two reads means the body or fields actually changed in between, not that someone commented. base_version is OPTIONAL: pass the version you last read and the write is gated by optimistic concurrency, compare-and-swap against the current row, so it only lands if nothing changed since. If the memo moved, the write is REJECTED with a conflict (409-equivalent) carrying current_markdown, current_version, and a severity (minor = 1 or 2 versions behind, same author; major = 3+ behind OR a different author wrote since). OMIT base_version only when you explicitly intend a last-writer-wins overwrite of whatever is there now. There is no separate lock taken; the CAS is the whole mechanism, so two writes that both omit base_version simply apply in arrival order.

url stringid stringkey stringdiff string*base_version integerconfirm_overwrite_changes integercomment_anchors arrayconfirm_orphan_count integer

list_memo_versions

List recorded versions of a memo, most recent first. Returns metadata only — version number, source ('web' / 'mcp'), op ('replace' / 'append'), author, comment-anchor audit (counts plus the thread IDs whose anchors couldn't be reattached on that write), and timestamps. Use get_memo_version to retrieve the markdown body for a specific version. Writes within a 30-second window from the same author + same source are coalesced into a single row, so quick agent loops show as one entry not twenty. Any role on the memo can read history.

url stringid stringkey stringlimit number

get_memo_version

Fetch the full markdown body of a specific version, plus its front-matter fields and the audit metadata. Useful when you want to see what the memo looked like before a particular edit, or to confirm what a previous agent run wrote.

url stringid stringkey stringversion_id string*

search_memos

sign-in required

Full-text search the authed user's memos. Returns up to limit (default 10, max 20) hits ranked by relevance, with a server-generated snippet around the match. Title hits rank higher than body hits. The LAST token of q is prefix-matched, so a type-ahead query like "pric" matches "pricing"; other tokens are AND-joined. Owner-scoped — only memos you own. Soft-deleted (trashed) memos are excluded. Requires OAuth.

q string*limit number

list_comments

List comments on a memo. Returns all threads in one call — page-level comments live under thread_id == 'page'; range-anchored comments use a generated thread id. For range threads the response includes an anchor snippet ({ before, range, after }) so the model can tell *which* passage a comment refers to (the range field is the highlighted text). Pass thread_id to scope to a single thread, e.g. when iterating replies. Resolved comments are returned by default; pass include_resolved: false to skip them. Assignment is thread-level (TRA-402): the fields are populated on the thread's first comment only — assigned_to_email (the invited user the thread is assigned to, or null), plus assigned_by_user_id and assigned_at. Use assign_comment / unassign_comment (which target the whole thread) to change them.

url stringid stringkey stringthread_id stringinclude_resolved booleaninclude_deleted booleanwith_anchor_text boolean

post_comment

sign-in required

Post a comment on a memo. Defaults to the page-level thread; pass thread_id of an existing range thread (obtained from list_comments) to reply on a specific passage. Pass parent_id to thread a reply under an existing comment. To start a NEW range-anchored thread (highlight a passage and comment on it, like selecting text in the editor), pass anchor_text: the exact text to highlight, copied from get_memo / read_section. If that text appears more than once, also pass context_before and/or context_after (the surrounding text) to pick the right occurrence — otherwise the call is rejected as ambiguous. The highlight is placed before the comment is saved, so a failed match never creates an empty thread. anchor_text is mutually exclusive with thread_id/parent_id. Requires Clerk auth (OAuth) and at minimum a comment-role URL.

url stringid stringkey stringbody string*thread_id stringparent_id stringanchor_text stringcontext_before stringcontext_after string

edit_comment

sign-in required

Edit the body of an existing comment. Only the comment's author or the memo owner may edit; other callers get forbidden. Sets edited_at. Use list_comments first to discover the comment_id.

url stringid stringkey stringcomment_id string*body string*

resolve_comment

sign-in required

Mark a comment resolved (or unresolve it with resolved: false). Same permission model as edit — author or memo owner only. Resolved comments stay visible in list_comments unless the caller passes include_resolved: false.

url stringid stringkey stringcomment_id string*resolved boolean

delete_comment

sign-in required

Soft-delete a comment. The row stays in the database (so the comment can be recovered server-side) but disappears from list_comments unless the caller passes include_deleted: true. Same permission model as edit/resolve — author or memo owner only.

url stringid stringkey stringcomment_id string*

assign_comment

sign-in required

Assign a comment THREAD to a user with Invited access to the memo (an email that has been invited via the share flow / share_memo). Assignment is thread-level, like Google Docs: pass any comment_id in the thread and the assignment lands on the whole thread (carried on its first comment), not that individual reply. Assigning to someone who hasn't been invited is rejected — invite them first. Pass a different assignee_email to reassign. The new assignee is emailed a confirmation. Use unassign_comment to clear, and list_comments to read the current assigned_to_email. Requires OAuth and at minimum a comment-role URL; only the comment's author or the memo owner may assign.

url stringid stringkey stringcomment_id string*assignee_email string*

unassign_comment

sign-in required

Clear a comment THREAD's assignment (sets assigned_to_email back to null). Thread-level, like assign_comment: pass any comment_id in the thread. No email is sent. Same permission model as assign_comment — author or memo owner. Use list_comments to discover the comment_id.

url stringid stringkey stringcomment_id string*

list_invited_users

sign-in required

List the people you can @mention and assign comments to on a memo — the memo owner plus collaborators invited at comment/edit role (read-only invitees are excluded). Returns [{ email, display }]. Use this to discover valid mention names and to pick an assignee_email for assign_comment. Requires OAuth and that you are the owner or a participant invitee.

url stringid stringkey string

add_reaction

sign-in required

Add an emoji reaction to a comment. Idempotent: adding the same reaction twice is a no-op (returns added: false). Useful as a lightweight temporal flag when reviewing or actioning comments: 👀 for 'reading', 🤔 for 'considering', ✅ for 'actioned', 🚫 for 'declining', ❤️ for 'acknowledged'. Avoids the noise of posting a reply just to say 'noted'. Requires OAuth and at minimum a comment-role URL on the memo. Use remove_reaction to undo. Use list_comments to discover comment_ids.

url stringid stringkey stringcomment_id string*emoji string*

remove_reaction

sign-in required

Remove a previously-added emoji reaction from a comment. Idempotent: if the reaction isn't present, the call is a no-op (returns removed: false). Only removes YOUR reaction — other users' reactions are unaffected. Use after add_reaction to clear a temporal flag once the work is done (e.g. swap 👀 for ✅). Requires OAuth and at minimum a comment-role URL.

url stringid stringkey stringcomment_id string*emoji string*

get_memo_image_upload_url

sign-in required

Mint a one-shot signed URL the agent can PUT a RASTER image to (PNG, JPEG, WebP, or GIF). Returns { upload_url, image_url, markdown, expires_at }. NETWORK REQUIREMENT — READ BEFORE INVOKING: The PUT in step 2 below requires outbound HTTPS from your sandbox/runtime to *.fieldmemo.io. Many MCP clients (notably Claude.ai web) sandbox the agent behind an egress proxy with an allowlist that does NOT include *.fieldmemo.io by default. If your PUT returns HTTP 403 with a response header like x-deny-reason: host_not_allowed, the block is in your runtime's egress proxy — NOT in Field Memo — and retrying will fail the same way. When that happens: DO NOT retry; DO NOT fall back to other upload methods (none of them work either); instead tell the user verbatim: "Image upload requires outbound network access to *.fieldmemo.io. On Claude.ai, this is in Settings → Privacy → Network (or similar in other MCP clients). Please enable egress to *.fieldmemo.io, then ask me to retry." Wait for the user to confirm before retrying. HOW TO USE THIS TOOL: 1. Call this tool with mime_type matching the image you'll upload. If the image is generated at higher resolution than its intended display size (e.g. an AI-generated 1024×1024 image meant to display at ~512px, or a screenshot from a retina display), pass pixel_density: 2 (or 3). Field Memo will render it at half (or third) its natural width so it stays sharp without overflowing the column. 2. From your sandbox / Bash, PUT the image bytes to upload_url with Content-Type: <mime_type>. Example: curl -X PUT --data-binary @file.png -H 'Content-Type: image/png' '<upload_url>'. The bytes go directly to our worker — they never traverse this MCP channel, never go through your output tokens, never get truncated by tool-call size caps. This is the only practical way to upload an image from an agent context. 3. On success the worker returns 200. The image is then live at image_url. Splice the returned markdown into the memo via append_to_memo, replace_section, etc. IMPORTANT — SVG is NOT supported (security policy: SVG can carry executable script). The URL is signed to the specific mime_type you request; PUTting bytes of a different format will be rejected. If you do not already have a raster image to upload, DO NOT try to fabricate one by generating SVG/XML markup — the PUT will be rejected. Instead, ask the user to upload the image themselves (drag-and-drop in the memo editor) or to give you a URL to an existing raster image (use upload_memo_image_from_url for that case). Diagrams or other visuals the user wants are usually better expressed as text/markdown content (lists, headings, tables) than as constructed images. The URL expires in 10 minutes and is single-use in spirit (re-PUT overwrites the same R2 object). Max body size 5 MB. Requires Clerk OAuth and edit role on the memo.

url stringid stringkey stringmime_type string*alt stringpixel_density integer

upload_memo_image_from_url

sign-in required

Upload a RASTER image (PNG, JPEG, WebP, or GIF) to a memo by giving the server a URL to fetch from. Use this when an image source URL is available (e.g. an image-generation tool returned a hosted URL) — it sidesteps the MCP message-size cap on the inline-base64 path. The server fetches the URL with strict guardrails (https only, our own zones / private IPs denied, manual redirects with per-hop revalidation, 5 MB cap, 10-second timeout) and stores the bytes as a memo image. Returns the hosted image URL plus pre-built markdown ready to splice into a follow-up write tool. IMPORTANT — source_url must point to a RASTER image. SVG is NOT supported (security policy) and will be rejected by the post-fetch MIME check. If the user is asking for a diagram or visual and you don't have a raster image URL, DO NOT try to fabricate one by generating SVG/XML — instead, ask the user to upload an image themselves or provide a URL to an existing raster image. Diagrams the user wants in their memo are usually better expressed as text/markdown content (lists, headings, tables) than as constructed images. Requires Clerk OAuth and edit role on the memo. Rate-limited to 60 fetches per hour per user.

url stringid stringkey stringsource_url string*alt stringmax_bytes number

get_memo_image_url

Get a short-lived (5-minute) signed URL that lets you download a raster image embedded in a memo. The memo's images are visible in its markdown as ![alt](https://img.fieldmemo.io/...), but those public URLs are behind hotlink protection and cannot be fetched from non-browser contexts. This tool mints a time-limited URL that bypasses that restriction. Pass the image_url exactly as it appears in the memo's markdown. The URL must be on img.fieldmemo.io and must belong to the memo you're reading. The returned read_url can be fetched with a plain GET (no auth headers needed). Requires any access role (read, comment, or edit) on the memo.

url stringid stringkey stringimage_url string*

get_memo_artifact_upload_url

sign-in required

Mint a one-shot signed URL the agent can PUT an HTML artifact to (self-contained HTML+CSS+JS, ≤1 MB). Returns { upload_url, artifact_url, markdown, expires_at }. WHAT IS AN ARTIFACT — interactive content the user wants to embed in their memo: a chart, a mini-app, a calculator, a visualisation, a tic-tac-toe board. The HTML you provide will render in a sandboxed iframe on a separate origin (fieldwerkartifacts.com) so it can run scripts safely without touching the user's Field Memo session. Same shape as Claude.ai's chat-side Artifacts. NETWORK REQUIREMENT — READ BEFORE INVOKING: the PUT requires outbound HTTPS from your sandbox to *.fieldmemo.io. If the PUT returns HTTP 403 with x-deny-reason: host_not_allowed, your runtime is blocking it — tell the user to enable outbound to *.fieldmemo.io in their MCP client's network settings (Claude.ai → Settings → Privacy → Network, or similar). Wait for them to confirm; don't retry until then. HOW TO USE THIS TOOL: 1. Call this tool. Returns upload_url (signed, 10-min TTL) and artifact_url (the eventual https URL the iframe will load from). 2. From your sandbox / Bash, PUT the HTML bytes to upload_url with Content-Type: text/html. Example: curl -X PUT --data-binary @artifact.html -H 'Content-Type: text/html' '<upload_url>'. Bytes go directly to our worker — never through your output tokens, never truncated by tool-call size caps. 3. Splice the returned markdown (a fenced ``artifact block) into the memo via append_to_memo / replace_section / etc. CONTENT REQUIREMENTS: • A self-contained HTML document. Inline <script> and <style> are fine. NO external scripts, stylesheets, or fonts — the artifact origin's CSP blocks them (script-src 'self' 'unsafe-inline', no CDNs). • NO network calls — connect-src 'none'. The artifact cannot fetch(), XHR, WebSocket, or WebRTC anywhere. • NO forms, popups, top-navigation, microphone, camera, geolocation, payment. Sandbox + Permissions-Policy lock these off. • Images must be inline (data: / blob:` URIs) or omitted. • Size cap: 1 MB. Keep artifacts focused — they're not full apps. If the user asks for something the artifact CSP can't allow (loading external data, making API calls), tell them so explicitly and suggest an alternative — don't silently strip the offending bits from the HTML and upload anyway. Requires Clerk OAuth and edit role on the memo.

url stringid stringkey stringheight integertitle string

list_collections

sign-in required

List collections owned by the authed user. Collections group memos into folder-like buckets; a memo can belong to more than one. By default archived collections are excluded — pass include_archived: true to see them too. Requires OAuth.

include_archived boolean

create_collection

sign-in required

Create a new collection owned by the authed user. NOT idempotent — if an active collection with the same (case-insensitive) name already exists, the call fails with a conflict error so agents can't fork. Lifecycle is just create / archive / unarchive (no delete from MCP); archive lives on the dashboard only. Requires OAuth.

name string*description string

rename_collection

sign-in required

Rename a collection owned by the authed user. The collection's memos and their memberships are untouched; only the display name changes. NOT idempotent against name clashes: if another active collection of yours already uses the new (case-insensitive) name, the call fails with a conflict error so two collections can't share a name. Pass the collection id (from list_collections) plus the new name. Requires OAuth, and you must own the collection.

collection string*name string*

archive_collection

sign-in required

Archive a collection owned by the authed user. Archiving is the app's way of removing a collection: it stops appearing in the chip-row picker on memos and drops out of list_collections (unless you pass include_archived: true). It is NOT a delete and is reversible. The collection's memos and their memberships are preserved, so nothing is unlinked or deleted, and you can restore it later with unarchive_collection. Pass the collection id (from list_collections). Requires OAuth, and you must own the collection.

collection string*

unarchive_collection

sign-in required

Restore a previously archived collection so it appears in list_collections and the memo chip-row picker again. Pass the collection id (use list_collections with include_archived: true to find archived ids). NOT idempotent against name clashes: if another active collection of yours took this collection's (case-insensitive) name while it was archived, the call fails with a conflict error, so rename one of them first. Requires OAuth, and you must own the collection.

collection string*

add_memo_to_collection

sign-in required

File a memo the authed user owns into one of their collections. Idempotent — re-adding a memo already in the collection is a no-op. Archived collections are rejected (unarchive first). Requires OAuth, and the caller must own both the memo and the collection.

memo string*collection string*

remove_memo_from_collection

sign-in required

Remove a memo from one of its collections. The memo and the collection themselves are untouched; only the membership is dropped. Idempotent — removing a memo not in the collection is a no-op. Requires OAuth.

memo string*collection string*

lock_memo

sign-in required

Lock a memo. While locked, no body, field, comment, or collection-membership writes succeed from any surface (REST, MCP, web). Reads continue to work normally and existing comments stay fully visible. Owner-only via Clerk OAuth. Idempotent: locking an already-locked memo returns the existing locked_at. To unlock, call unlock_memo and follow the returned URL — unlock cannot be done from MCP because the unlock action requires a human confirmation in the browser.

url stringid string

unlock_memo

sign-in required

Initiate unlocking a memo. Does NOT unlock from this tool — returns a one-shot URL the user must open in their browser, where the unlock confirmation dialog pops automatically and they have to click Unlock to confirm. This friction is deliberate: a locked memo is a protective intent the owner set, and an agent shouldn't be able to undo it for them. Owner-only via Clerk OAuth. Use when the user asks you to unlock a memo. RELAY THE RETURNED unlock_url VERBATIM TO THE USER and tell them to open it; do not retry, do not call any write tool against this memo until they confirm they've unlocked it.

url stringid string