> ## 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.

# Reading & editing proposals

> Read the document as DSL, then insert, update, delete, reorder, or find &amp; replace.

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](/mcp/theming#per-section-overrides).

## Concurrency

All mutation tools share the same optimistic-concurrency mechanism:

<Steps>
  <Step title="Snapshot">
    Read the current document and `updated_at`.
  </Step>

  <Step title="Mutate locally">
    Apply the requested change.
  </Step>

  <Step title="Update with guard">
    `UPDATE proposals SET json = ... WHERE id = ? AND updated_at = ?`.
  </Step>

  <Step title="Retry on conflict">
    If `updated_at` shifted (someone else wrote concurrently), refresh and
    retry. Up to 3 attempts.
  </Step>

  <Step title="Surface conflict">
    On 3 consecutive conflicts: returns
    `Proposal changed too quickly. Please retry the append.` — the agent
    should refetch and try again.
  </Step>
</Steps>

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

<AccordionGroup>
  <Accordion title="Indices shift after mutation — always refetch">
    `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.
  </Accordion>

  <Accordion title="Do I need to refetch after find_and_replace?">
    No — `find_and_replace_in_proposal` doesn't change the structure, only
    text content inside nodes. Indices stay stable.
  </Accordion>

  <Accordion title="Insert at index = doc.length appends">
    Yes. `index = 0` prepends, `index = doc.length` appends, anything in
    between inserts before the existing item at that position.
  </Accordion>

  <Accordion title="Updating a non-section item">
    `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.
  </Accordion>

  <Accordion title="Bulk operations">
    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.
  </Accordion>
</AccordionGroup>
