MCP tools reference
The 98 toolsthe Fieldwerk 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 — that includes every tool that creates, changes, or deletes anything. The rest are read tools that also work against a memo's shared link.
connect
Check whether the user is connected to a Fieldwerk account, and get the link to connect if they are not. Call this whenever a tool reports the user is not signed in, or when the user asks to connect or sign in. Reading shared memos works without sign-in; creating or editing anything requires a connected account. When connected is false, give the user connect_url, ask them to sign in and approve access, then retry the original tool. Many MCP clients also open the authorisation page automatically on the first call that needs it.
create_memo
sign-in requiredCreate a new Fieldwerk memo on the user's account. Requires the user's Fieldwerk account to be connected; an unauthenticated call returns an authentication challenge, and the connect tool provides the sign-in link. Returns the memo's single share URL in url. 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 Fieldwerk 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.fieldwerk.ai/m/abc123?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.
get_memo
Fetch a memo by URL or id. Returns markdown and metadata, including archived_at (null = live). An archived memo still reads fine for workspace members but rejects every write with memo_archived until someone unarchives it, so check archived_at before planning edits. 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.
update_memo
sign-in requiredReplace 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).
CONCURRENCY-SAFE MERGE (recommended): whenever you pass base_version, ALSO pass merge_base_markdown = the exact markdown you got back from that same get_memo. If another writer changed a DIFFERENT part of the memo since your read, the server 3-way merges your rewrite onto their current version (using your read as the common base): their edit is preserved AND your change lands, with no conflict. A genuine same-lines overlap still returns a conflict for you to resolve. This is the difference between your whole-doc rewrite co-existing with a live editor and bouncing off (or clobbering) them — always send it. 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: 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.
append_to_memo
sign-in requiredAppend 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 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.
list_my_memos
sign-in requiredList memos owned by the authed user. Optionally filter by front-matter fields via where: an array of { field, equals } clauses (all must match). Pass notebook (an id, or a name when combined with collection) to scope to a notebook, a sub-group of memos inside one collection. Spans ALL your workspaces by default (each result is tagged with its own workspace); pass workspace (name, slug, or id from list_workspaces) to scope to one. Does NOT follow the web app's current workspace. Archived memos are excluded unless include_archived: true (included rows carry archived_at). Paginate with limit (default 50, max 1000) and offset (default 0): pass offset 50 for the second page of 50, and so on. Requires OAuth.
delete_memo
sign-in requiredDelete a memo. CLAIMED memos go to the user's trash for 30 days (recoverable from /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, edit access via its link is enough. If the memo is claimed, you must be signed in via OAuth as the owner — edit access 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.
claim_memo
sign-in requiredAttach an orphan memo to your account. Requires OAuth. Pass the memo's edit URL (or id).
duplicate_memo
sign-in requiredCOPY a memo into a brand new memo (new id, new URLs); the original is left in place. To RELOCATE an existing memo to another workspace without copying it (keeping the same id, history, and comments), use move_memo instead. The copy includes content, fields, images, embedded artifacts, and optionally comments. Any role (viewer, commenter, editor) can duplicate. Returns the new memo's role URLs. The copy lands in a workspace: pass workspace (name, slug, or id from list_workspaces) to choose; with keep_collections it adopts the source collections' workspace instead. If you belong to multiple workspaces and pass neither, the call returns a workspace_required disambiguation that lists them (it will NOT silently use whatever workspace you last opened in the web app). Ask the user which one, then retry with an explicit workspace, and remember their choice for the rest of this session so later writes pass it without asking again.
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.
replace_section
sign-in requiredReplace 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. 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.
rename_heading
sign-in requiredRename 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. 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.
append_to_section
sign-in requiredAppend 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.
add_list_item
sign-in requiredAppend 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.
toggle_task
sign-in requiredToggle 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.
remove_list_item
sign-in requiredRemove 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.
add_table_row
sign-in requiredAppend 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.
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.
get_field
Read a single front-matter field by key.
set_field
sign-in requiredSet 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.
rename_field
sign-in requiredRename 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. state → status). 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.
delete_field
sign-in requiredRemove 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.
patch_memo
sign-in requiredApply 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. The 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.
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.
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.
search_memos
sign-in requiredFull-text search the memos you can read — your own AND ones shared to you (a direct/group grant, or a memo in a collection you can access). It never returns a memo you couldn't open. 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. Soft-deleted (trashed) memos are excluded, and archived memos are excluded unless include_archived: true. Pass collection (an id, slug, or name of a collection you can access) to scope the search to memos filed in it. Pass notebook (an id, or a name when combined with collection) to scope to a notebook you own, a sub-group of memos inside one collection. Paginate through the ranked hits with limit (default 10, max 20) and offset (default 0): pass offset 10 for the next page. Requires OAuth.
list_inbox
sign-in requiredYour agent inbox (TRA-614): comment threads that @mention THIS connected app, PLUS new activity on threads you're already working (any thread you've posted on or been mentioned in resurfaces when someone else comments — never your own messages). Across all your memos or scoped to one memo/notebook/collection/workspace. Each item gives the type (comment_mention | thread_activity), the memo (id + url), the thread and its status (open/resolved), and the latest message with who wrote it, whether it was an agent, and whether YOU wrote it (by_me) — so you can decide whether to act WITHOUT polling every memo, and skip threads where you already had the last word. Poll incrementally: pass the previous response's cursor back as since to get only what's new. Returns unacked items only unless include_acked=true; ack items with ack_inbox once handled. Requires a connected MCP app (OAuth).
ack_inbox
sign-in requiredMark inbox items handled so they drop out of your default list_inbox feed (TRA-614). Ack a single item by notification_id, or every item for a thread by thread_id (e.g. once you've replied in that thread). Idempotent. Requires a connected MCP app (OAuth).
set_working
sign-in requiredSignal that YOU (this connected app) are actively working a comment thread, so the humans watching it see "<your app> is working on this…" instead of silence while you read and compose (TRA-614). Call it as soon as you pick a thread up. It auto-expires after ttl_seconds (default 600 = 10 min) — call again to keep it alive (a heartbeat) on long work, and clear_working when you're done. If you crash or forget, it evaporates on its own, so it can never get stuck. Requires a connected MCP app (OAuth). Needs comment access to the memo.
clear_working
sign-in requiredClear your "working on it" signal on a thread once you've finished (or handed off), so the badge drops immediately rather than waiting for it to expire (TRA-614). Idempotent. Requires a connected MCP app (OAuth).
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. Soft-deleted comments stay hidden; include_deleted: true is honored only when you are the memo owner. 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. If any connected app is actively working a thread right now, a top-level working array lists them ({ thread_id, display, expires_at }) — check it before jumping into a thread so you don't stomp another agent (set your own with set_working).
post_comment
sign-in requiredPost 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.
edit_comment
sign-in requiredEdit 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.
resolve_comment
sign-in requiredMark a comment resolved (or unresolve it with resolved: false). Anyone with edit access to the memo can resolve/unresolve a thread (not just the comment author or the memo owner). Resolved comments stay visible in list_comments unless the caller passes include_resolved: false.
delete_comment
sign-in requiredSoft-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 editing a comment: author or memo owner only.
assign_comment
sign-in requiredAssign a comment THREAD to a user with Invited access to the memo (an email that has been invited via the share flow / share_memo), OR to one of YOUR OWN connected apps by passing assignee_client_id instead (so a human sees "Assigned to <app>" and that app gets a comment_assignment in its inbox). Pass exactly one of assignee_email / assignee_client_id. 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 — or an app you haven't connected — is rejected. Pass a different assignee to reassign. A new human assignee is emailed a confirmation. Use unassign_comment to clear, and list_comments to read the current assigned_to_email / assigned_to_client_id. Requires OAuth and at minimum a comment-role URL; only the comment's author or the memo owner may assign.
unassign_comment
sign-in requiredClear 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.
list_invited_users
sign-in requiredList 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.
add_reaction
sign-in requiredAdd 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.
remove_reaction
sign-in requiredRemove 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.
get_memo_image_upload_url
sign-in requiredMint 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 *.fieldwerk.ai. Many MCP clients (notably Claude.ai web) sandbox the agent behind an egress proxy with an allowlist that does NOT include *.fieldwerk.ai 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 Fieldwerk — 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 *.fieldwerk.ai. On Claude.ai, this is in Settings → Privacy → Network (or similar in other MCP clients). Please enable egress to *.fieldwerk.ai, 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). Fieldwerk 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.
upload_memo_image_from_url
sign-in requiredUpload 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.
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 , 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.fieldwerk.ai 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.
get_memo_artifact_upload_url
sign-in requiredMint 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 Fieldwerk session. Same shape as Claude.ai's chat-side Artifacts.
NETWORK REQUIREMENT — READ BEFORE INVOKING: the PUT requires outbound HTTPS from your sandbox to *.fieldwerk.ai. 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 *.fieldwerk.ai 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.
list_workspaces
sign-in requiredList the workspaces you belong to. Use a workspace's name, slug, or id as the workspace argument on create_memo, create_collection, duplicate_memo, move_memo, list_my_memos, search_memos, and list_collections to act in a specific one. active marks your current workspace in the web app, which READS default to. WRITES that place data (create_memo, create_collection, duplicate_memo, move_memo) do NOT default to it when you belong to several workspaces: pass workspace explicitly, or you'll get a workspace_required ask (then remember the user's choice for the session). If this connection is pinned to a workspace via its URL, pinned_to names it and every tool acts there. Requires OAuth.
set_active_workspace
sign-in requiredSet your ACTIVE workspace — where new memos and collections go by default when you don't pass an explicit workspace. Mirrors the web app's workspace switcher and persists (it also changes the active workspace in the web app and on your other unpinned connections). For a one-off in a different workspace, prefer passing workspace on the individual tool instead of switching. Pass a workspace name, slug, or id from list_workspaces. Requires OAuth.
list_collections
sign-in requiredList collections the authed user can access — the ones they own PLUS the ones shared to them (a workspace share, or a direct/group grant), matching what the web app shows. Each row carries owned (true = yours) and can_manage (may rename/archive/share). 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 your archived ones too (shared collections are active-only). Spans ALL your workspaces by default (each result is tagged with its own workspace); pass workspace (name, slug, or id from list_workspaces) to scope to one. Does NOT follow the web app's current workspace. Requires OAuth.
create_collection
sign-in requiredCreate 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. The collection lands in a workspace: if you belong to multiple workspaces and don't pass workspace, the call returns a workspace_required disambiguation listing them (it will NOT silently use whatever workspace you last opened in the web app). Ask the user which one, retry with an explicit workspace, and remember their choice for the rest of this session so later writes pass it automatically. Requires OAuth.
clone_collection
sign-in requiredClone a PUBLISHED collection into one of your workspaces. Deep-copies the collection and its memos — content, images, and artifacts — but NOT comments, and records where it came from. You must be signed in and not already a member of the source collection's workspace, and the owner must have enabled cloning on it. This is the server-side, lossless equivalent of recreating it by hand (images/artifacts can't be copied manually). Pass collection as the published collection's URL or id. Optionally: workspace (name/slug/id) to choose where it lands (defaults to your active workspace), name to rename the copy, memo_ids to copy a subset (defaults to all), and password if the collection link is password-protected. Requires OAuth.
rename_collection
sign-in requiredEdit a collection's name and/or description. Allowed for the owner OR a manager (workspace owner/admin, a workspace-wide Manage share, or an admin grant) — the same set that may share it. The memos and their memberships are untouched. Pass the collection id (from list_collections, where can_manage tells you if you may) plus a new name and/or description (empty string clears the description). NOT idempotent against name clashes: if another active collection with the owner's (case-insensitive) name already exists in the workspace, the call fails with a conflict. Requires OAuth.
archive_collection
sign-in requiredArchive 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). Allowed for the owner OR a manager (see can_manage). Requires OAuth.
unarchive_collection
sign-in requiredRestore 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. Allowed for the owner OR a manager (see can_manage). Requires OAuth.
add_memo_to_collection
sign-in requiredFile a memo into a collection. Filing does NOT change the memo's workspace, it stays where it is. To MOVE a memo to a different workspace, use move_memo instead. Idempotent — re-adding a memo already in the collection is a no-op. Archived collections are rejected (unarchive first). A memo MAY belong to several collections, including more than one PUBLISHED collection — membership is not exclusive and nothing is moved or removed. When the memo is filed into a published collection while it also lives elsewhere, the result includes a note that it is now reachable through this collection's link too. Requires OAuth: EDIT access to the memo AND EDIT access to the collection. Filing is an edit action, so ownership is NOT required — manage-level access is only for sharing, invites, and publish.
remove_memo_from_collection
sign-in requiredRemove 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.
list_notebooks
sign-in requiredList the notebooks inside a collection you can access — the ones you own AND ones shared to you, including view-only shares (listing is a read, so it doesn't require edit access). A notebook is a named sub-group of memos within one collection (a divider tab inside it). Pass collection as an id, slug, or name. By default archived notebooks are excluded; pass include_archived: true to see them too. Requires OAuth.
create_notebook
sign-in requiredCreate a notebook inside a collection you own or can edit. NOT idempotent: if an active notebook with the same (case-insensitive) name already exists in that collection, the call fails with a conflict so agents can't fork. Pass collection (id, slug, or name) plus a name. Lifecycle is create / archive / unarchive (no delete from MCP). Requires OAuth.
rename_notebook
sign-in requiredEdit a notebook's name, description, and/or standing instructions. The memos and their memberships are untouched. Pass the notebook id (from list_notebooks or get_memo) plus a new name, description, and/or instructions (empty string clears description or instructions). instructions are author-written standing instructions handed to an agent working in this notebook. NOT idempotent against name clashes within the collection. Requires OAuth and edit access to the parent collection.
archive_notebook
sign-in requiredArchive a notebook. Archiving drops it from list_notebooks (unless you pass include_archived: true) but is NOT a delete and is reversible with unarchive_notebook. The notebook's memos and their memberships are preserved. Pass the notebook id. Requires OAuth and edit access to the parent collection.
unarchive_notebook
sign-in requiredRestore a previously archived notebook so it appears in list_notebooks again. NOT idempotent against name clashes: if another active notebook in the collection took its (case-insensitive) name while it was archived, the call fails with a conflict. Pass the notebook id. Requires OAuth and edit access to the parent collection.
add_memo_to_notebook
sign-in requiredAdd a memo to a notebook. A notebook member must live in the notebook's parent collection, so this AUTO-ADDS the memo to that collection if it isn't already in it (the response says so). Idempotent. Pass memo and notebook ids, and optional pinned. Requires OAuth, edit access to the memo, and edit access to the parent collection.
remove_memo_from_notebook
sign-in requiredRemove a memo from one notebook. The memo stays in the parent collection (use remove_memo_from_collection to remove it from the collection, which also removes it from every notebook of that collection). Idempotent. Pass memo and notebook ids. Requires OAuth, edit access to the memo, and edit access to the parent collection.
set_notebook_pin
sign-in requiredPin or unpin a memo within a notebook. Pinned members sort first in the notebook's list views. The memo must already be a member of the notebook (pinning a non-member is a no-op). Idempotent. Pass memo and notebook ids plus pinned (true to pin, false to unpin). Requires OAuth, edit access to the memo, and edit access to the parent collection.
lock_memo
sign-in requiredLock 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. Available to the owner or a member/collaborator with edit access, 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.
unlock_memo
sign-in requiredInitiate 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.
Available to the owner or a member/collaborator with edit access, 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.
archive_memo
sign-in requiredArchive a memo. The memo stays in place with access and memberships intact, but becomes read-only: no body, field, comment, or collection-membership writes succeed from any surface until it is unarchived. It is hidden from default lists and searches (pass include_archived: true to list tools to see it) and never appears in published collections. NOT the same as locking: archive is an organisational state, the lock is a protective one, and either freezes writes on its own. Available to the owner or a member/collaborator with edit access, via Clerk OAuth. Idempotent: archiving an already-archived memo returns the existing archived_at. Reversible with unarchive_memo.
unarchive_memo
sign-in requiredUnarchive a memo: restore it to the normal editable state and back into default lists. The counterpart to archive_memo. Unlike unlock_memo this DOES perform the change: archive is an organisational state, not a protective one, so no browser confirmation is required. Available to the owner or a member/collaborator with edit access, via Clerk OAuth. Idempotent: unarchiving a memo that isn't archived is a no-op.
get_memo_sharing
sign-in requiredRead a memo's full sharing posture (owner only): the public link level + password, the restricted flag, every direct grant (people and groups with their level), the collections it's shared through, and any pending email invites. Use this BEFORE changing sharing so you can report the current state. A non-owner gets an access error. Requires OAuth.
set_memo_link
sign-in requiredSet the memo's PUBLIC share-link level, and optionally its password (owner only). access: none (link off) / view / comment / edit — anyone with the link gets that level. password: a non-empty string requires it, an empty string clears it, omit to leave it unchanged. Owner-only. Requires OAuth.
set_memo_grant
sign-in requiredGrant (or update) a specific person or group access to a memo, silently — no email is sent (use share_memo to invite by email with a note). Owner only. Identify the subject with user_email (must be an existing Fieldwerk account) OR group (id or name within the memo's workspace). level: view / comment / edit. Re-granting updates the level. Requires OAuth.
revoke_memo_access
sign-in requiredRemove a person's or group's direct grant on a memo (owner only). Identify with user_email or group (as in set_memo_grant). Idempotent — removing a subject that has no grant is a no-op. Does NOT affect the public link or collection-derived access. Requires OAuth.
set_memo_restricted
sign-in requiredTurn a memo's invited-only (restricted) mode on or off (owner only). This is the same control as the web share dialog's "Share via collections" toggle. When restricted: true, only people with a direct grant (and the owner) reach the memo — the collection channel is turned OFF, which also REMOVES the memo from every collection it's in (it then can't be added to one until you lift the restriction). The public link and direct grants are left exactly as they are — to also turn the link off, call set_memo_link with access: "none". restricted: false clears it so the memo can be filed into collections again. Requires OAuth.
transfer_memo_ownership
sign-in requiredTransfer a memo to a new owner (CURRENT owner only). This changes only the owner, the memo stays in its current workspace. To MOVE a memo to a DIFFERENT workspace (optionally handing it to someone there at the same time), use move_memo instead. The new owner must be an existing Fieldwerk account (new_owner_email). You (the previous owner) are dropped to an Editor grant — you keep edit access but lose ownership and management. This is a deliberate, hard-to-undo action; confirm intent before calling. Requires OAuth.
move_memo
sign-in requiredMove a memo to another workspace (CURRENT owner only). This is a MOVE, not a copy: the memo keeps its same id and its full edit history, versions, comments, reactions, images, and embedded artifacts all come with it. After the move the memo is removed from the source workspace entirely, and its link sharing and direct grants are reset to the destination's defaults. Identify the memo with url (or id). Pass workspace for the destination (a name, slug, or id from list_workspaces); it must differ from the memo's current workspace, and is required (a move places the memo into a specific workspace, never the one you last opened in the web app). When the user belongs to several workspaces, ask which destination they mean before calling, and remember that choice for the rest of the session. By default you become the new owner; pass new_owner_email to hand it to another member (they must be an active, non-guest member of the destination, as must you). Optionally file it into destination collections: collection_ids for existing ones, new_collections to create fresh ones (each a name, or { name, description? }), or recreate_source_collections: true to recreate the memo's current collections (name AND description) in the destination. New collections are owned by the new owner by default; pass new_collection_owner_email to assign them to another member (find people with list_workspace_members). Existing collections are left untouched. This is a deliberate, hard-to-undo action; confirm intent before calling. Requires OAuth.
list_workspace_members
sign-in requiredList the people in a workspace you can hand things to — active, non-guest members only (owners, admins, editors; guests are excluded). Returns [{ email, display, role }]. Use it to pick a new_owner_email or new_collection_owner_email for move_memo, or for any other 'assign to a member' choice. With one workspace it uses that; if you belong to several it won't guess (it does not follow the web app's current workspace), so pass workspace (a name, slug, or id from list_workspaces). Requires OAuth.
get_collection_sharing
sign-in requiredRead a collection's sharing posture (manager only): whether it's published (read-only link) plus its slug and password, the workspace-wide share level, and every direct grant (people/groups with their level). Use this before changing a collection's sharing. Requires OAuth.
list_collection_memos
sign-in requiredList every memo in a collection you own or that is shared with you, most recently active first. The access-aware twin of the web collection drill-in: identify the collection by collection (an id, slug, or name), and you get back the memos you're allowed to see in it (the owner sees all; a shared viewer sees the non-restricted memos plus any they were invited to). Unlike search_memos, this needs no query and is not owner-only, so it's the right tool to enumerate a collection a teammate shared with you. Returns each memo's id, title, a URL to open it, and its updated/last-activity times. Archived memos are excluded unless include_archived: true (included rows carry archived_at). Paginate with limit (default 50, max 1000) and offset (default 0): pass offset 50 with limit 50 for the second page. Requires OAuth.
publish_collection
sign-in requiredPublish or unpublish a collection as a READ-ONLY public link (manager only). access: "view" publishes (anyone with the link reads its memos read-only), "none" unpublishes. Optionally set slug (the /c/<slug> alias; empty string clears it) and password (empty string clears it). A collection can only be published read-only — commenting and editing live on each memo's own link. Returns url, the public link to hand to the user (null when unpublished). Requires OAuth.
set_collection_grant
sign-in requiredGrant (or update) a person or group access to a collection (manager only). Identify with user_email (existing account) or group (id or name in the collection's workspace). level: view / comment / edit / admin. Granting admin is OWNER-only (admins can't promote other admins). Requires OAuth.
revoke_collection_access
sign-in requiredRemove a person's or group's direct grant on a collection (manager only). Identify with user_email or group. Idempotent. Does NOT change the workspace-wide share or the published link. Requires OAuth.
restrict_collection
sign-in requiredRestrict a collection: set its workspace share to none (only people with a direct grant, and the owner, reach it) and, by default, also turn its public link off (mute_link: false keeps the link). Manager only. To un-restrict, use set_collection_workspace_share with a level. Requires OAuth.
transfer_collection_ownership
sign-in requiredTransfer a collection to a new owner (CURRENT owner only). The new owner must be an existing Fieldwerk account in the collection's workspace (new_owner_email). You (the previous owner) drop to an Editor grant. This is hard to undo; confirm intent before calling. Requires OAuth.
restore_memo
sign-in requiredRestore one of YOUR memos from the trash (undo a delete). Owner-only; the memo must currently be trashed. Idempotent — restoring a memo that isn't trashed is a no-op. An ARCHIVED memo unarchives on restore by default so it comes back visible and editable; pass keep_archived: true to restore it still archived (read-only, hidden from default lists). The counterpart to delete_memo. Requires OAuth.
empty_trash
sign-in requiredPermanently delete YOUR trashed memos in ONE workspace. This is IRREVERSIBLE — the memos and their content are hard-deleted, not recoverable. Returns the count and ids removed. Confirm intent with the user before calling.
Scope: only memos YOU own are ever touched, never a teammate's trashed memos in the same workspace. Pass workspace (a name, slug, or id from list_workspaces) to say WHICH workspace's trash to empty. If you belong to several and pass none, the call returns a workspace_required disambiguation listing them rather than sweeping all of them — ask the user which they mean and retry with an explicit workspace. There is no way to list the trash over MCP first, so this deliberately will not guess. Requires OAuth.
set_collection_favorite
sign-in requiredFavorite or unfavorite one of your collections (owner only). Favorited collections pin to the top of your sidebar. favorite: true favorites, false unfavorites. Identify the collection by id, slug, or name. Requires OAuth.
set_default_collection
sign-in requiredMake one of your collections the default that NEW memos are filed into (owner only; the default is per workspace). The collection can't be archived. Identify it by id, slug, or name. Requires OAuth.
get_asset_upload_url
sign-in requiredMint a one-shot signed URL to upload a FILE and attach it as an asset to a memo, collection, or notebook (provide exactly one target). Returns { asset_id, upload_url, expires_at }.
HOW TO USE: 1) call this with the file's content_type (and ideally filename); 2) from your sandbox/Bash, PUT the bytes to upload_url with a matching Content-Type header, e.g. curl -X PUT --data-binary @file.pdf -H 'Content-Type: application/pdf' '<upload_url>'. Bytes go straight to our worker — they never traverse this MCP channel. On success (HTTP 200) the asset is live and listed by list_assets / get_memo. The PUT needs outbound HTTPS to *.fieldwerk.ai.
Limits: 25 MB; allowed types include pdf, office docs, csv/tsv, txt/md, json, zip, common audio/video, and images. html/svg/executables are rejected. URL expires in 10 minutes. Requires OAuth + edit access to the target.
add_link_asset
sign-in requiredAttach a LINK (a URL) as an asset to a memo, collection, or notebook (exactly one target). Optionally set title/description yourself, and a free-text summary (a short write-up of what's at the link — this is the field your assistant is expected to author). No automatic fetching is done. Requires OAuth + edit access to the target.
add_asset_from_url
sign-in requiredAttach a FILE to a memo, collection, or notebook (exactly one target) by giving the server a URL to fetch — use this when you already have a hosted file URL (it sidesteps the MCP message-size cap and you don't need to PUT bytes yourself). The server fetches the URL with strict guardrails (https only, our own zones / private IPs denied, manual redirects, 25 MB cap, 10-second timeout) and stores it as an asset. You MUST pass content_type (must be an allowed type: pdf, office docs, csv/tsv, txt/md, json, zip, audio/video, images). Returns the created asset. Requires OAuth + edit access to the target.
backfill_link_asset
sign-in requiredFetch a LINK asset's URL with a basic, NON-AI request and fill its empty title/description from the page's <title> / Open Graph / meta tags. Only fills fields you haven't already set; always records what was fetched. Requires edit access to the asset.
list_assets
sign-in requiredList the assets (files + links) attached to a memo, collection, or notebook (exactly one target). Requires read access to that target.
get_asset
sign-in requiredGet one asset's metadata by id. For file assets the response includes a short-lived download_url. Readable if you can read any parent it's attached to.
attach_asset
sign-in requiredAttach an EXISTING asset to another memo, collection, or notebook (exactly one target). Gated by edit access to that target.
remove_asset
sign-in requiredDetach an asset from a memo, collection, or notebook (exactly one target). The asset itself is not deleted — use delete_asset for that. Gated by edit access to the target.
delete_asset
sign-in requiredPermanently delete an asset (owner only). Removes it from every parent and frees its storage. Use remove_asset to only detach from one place.
query_asset
sign-in requiredQuery a CSV/TSV FILE asset by id: optional column projection, simple AND filters (eq / neq / contains), and a row limit. Returns matching rows as JSON plus matched (total before limit). Readable if you can read the asset. Server-side parse, capped at 500 rows — for large datasets pull the columns/filters you need rather than the whole file.