Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.propal.io/llms.txt

Use this file to discover all available pages before exploring further.

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

get_proposal_tree            ← read current state as DSL

plan mutations

insert_section_at_index      ← or update / delete / reorder /
                                set_proposal_section_attrs /
                                find_and_replace_in_proposal

get_proposal_tree            ← refetch (indices may have shifted)
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

get_proposal_tree
  proposal_id: '<uuid>'
→ {
    items: [
      { index: 0, kind: 'section', dsl: { type: 'section', ... } },
      { index: 1, kind: 'section', dsl: { type: 'section', ... } },
      { index: 2, kind: 'other',  fallback: { tiptap_type, text_preview, child_count } }
    ],
    doc_size_chars: 12894
  }
  • 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

insert_section_at_index
  proposal_id: '<uuid>'
  index: 0                      // 0 = prepend; doc.length = append
  tree: { type: 'section', children: [...] }
  // or [section, section]
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

update_section_at_index
  proposal_id: '<uuid>'
  index: 1
  tree: { type: 'section', ... }   // 1 → 1
  // or [section, section]         // 1 → N (document grows)
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

delete_section_at_index
  proposal_id: '<uuid>'
  index: 3
Removes a single root item. Works on kind: 'other' items too.

Reorder

reorder_sections
  proposal_id: '<uuid>'
  order: [2, 0, 1]                 // permutation of current indices
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

find_and_replace_in_proposal
  proposal_id: '<uuid>'
  find: 'old text'
  replace: 'new text'
  options: {
    case_sensitive: false,        // default false
    whole_word: false              // default false; uses Unicode word boundaries
  }
→ { replacements: 7 }
  • 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:
set_proposal_section_attrs
  proposal_id: '<uuid>'
  index: 0
  attrs: {
    section_style: 'highlight',     // or "default" or "1".."N"
    theme: 'gray',                   // legacy, optional
    background_image: { media_id, overlay, overlay_opacity, ... }
  }
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.