Skip to main content
The MCP exposes a complete read/edit toolkit on top of the components DSL. An agent can walk an existing proposal, target sections by index, mutate them atomically, and re-fetch as needed.

The lifecycle

Mutations are independent — there is no multi-tool transaction. After each mutation, indices in the document may shift, so the agent must refetch before issuing the next.

Read

  • kind: 'section' — the disassembler produced a DSL section. dsl is the authoring shape used by insert/update.
  • kind: 'other' — a root-level node that isn’t a section (legacy or manual edit). fallback carries enough info (text preview, child count) to let the agent decide whether to delete or replace it.
Reading is best-effort: nodes the disassembler can’t map come back as fallback. To preserve a complex node, prefer find_and_replace or delete_section_at_index + a fresh insert.

Reverse mapping niceties

  • Old proposals containing testimonialsContainer / accordionContainer / etc. are flattened to the grouped DSL form (testimonial_cards, questions, …) on read.
  • Section slots referenced positionally are converted back to named ids if the applied theme has them (section_style: "2""deep").
  • Assembler defaults are stripped on read so DSL ↔ TipTap ↔ DSL is a fixed point for every component the DSL covers.

Insert

Validates the tree with the same rules as append_propal_component_tree_to_proposal, resolves named section_style refs against the proposal’s theme, then inserts BEFORE index.

Update

Replaces the item at index by the assembled section(s). 1 → N is supported — useful when you want to split a single section into several.

Delete

Removes a single root item. Works on kind: 'other' items too.

Reorder

order MUST contain every integer in [0, length-1] exactly once and have the same length as the document. The new arrangement matches the order of values in order: new[0] = old[2], new[1] = old[0], etc.

Find & replace

  • Literal matching only — no regex.
  • Walks all text nodes recursively, preserving marks (bold, italic, …).
  • Short-circuits when 0 replacements — no Supabase write, no updated_at bump.
  • Whole-word matching is Unicode-aware (\p{L}\p{N}_ boundaries) so French accents work correctly.

Patching section attrs

To change a section’s look without touching its children, use the dedicated patch tool:
See theming.

Concurrency

All mutation tools share the same optimistic-concurrency mechanism:
1

Snapshot

Read the current document and updated_at.
2

Mutate locally

Apply the requested change.
3

Update with guard

UPDATE proposals SET json = ... WHERE id = ? AND updated_at = ?.
4

Retry on conflict

If updated_at shifted (someone else wrote concurrently), refresh and retry. Up to 3 attempts.
5

Surface conflict

On 3 consecutive conflicts: returns Proposal changed too quickly. Please retry the append. — the agent should refetch and try again.
This means: even if two agents (or an agent + a human in the editor) work on the same proposal at once, you’ll never silently overwrite each other. The downside is the agent must retry rather than queue — there’s no batch mutation API.

Pitfalls

insert_section_at_index(0, X) then delete_section_at_index(2) does NOT delete the original index 2 — the original index 2 is now at index 3 because of the insert. Always refetch via get_proposal_tree between mutations.
No — find_and_replace_in_proposal doesn’t change the structure, only text content inside nodes. Indices stay stable.
Yes. index = 0 prepends, index = doc.length appends, anything in between inserts before the existing item at that position.
update_section_at_index accepts any index — even a kind: 'other' item. The result is a section (or several) replacing the legacy node, which is usually what you want.
There’s no bulk_delete or bulk_insert. Run mutations sequentially — the optimistic guard handles concurrent writers, but each mutation is its own atomic operation.