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

# Limits & errors

> Validation rules, structural limits, and how the MCP reports errors.

## Component DSL limits

Exposed in the `limits` field of `list_propal_components`:

| Limit                    | Value | Meaning                                         |
| ------------------------ | ----- | ----------------------------------------------- |
| `MAX_SECTIONS`           | 10    | Sections per single `append` call               |
| `MAX_DEPTH`              | 6     | Max nesting depth (columns inside columns…)     |
| `MAX_NODES`              | 200   | Total nodes per tree (assembler-side count)     |
| `MAX_BLOCKS_PER_SECTION` | 50    | Direct children of a single `section`           |
| `MAX_BLOCKS_PER_COLUMN`  | 30    | Children inside one column of a `columns` block |
| `MAX_COLUMNS`            | 4     | `columns.cols` upper bound                      |

If the agent exceeds a limit, validation rejects the tree with an explicit
error like `tree exceeds MAX_NODES (200)`.

## Structural rules

Enforced both by Zod and a runtime validator (so you get a clear message,
not a generic Zod failure):

* Root must be a `section` or `section[]`
* A `section` cannot contain another `section`
* `columns.cols` must equal `columns.length`
* `embed.src` must match the provider's host allowlist (HTTPS only)
* `image` / `icon` / `video` cannot have a `src` (placeholders only)

## Theme validation

* `themes.section_styles[].id` is optional, but if present must be 1-32 chars
  matching `^[a-zA-Z][a-zA-Z0-9_-]*$`.
* Reserved ids (rejected): `"default"` and any digit-only string.
* Ids must be **unique** within a single theme — duplicates → `BAD_REQUEST`.
* `apply_theme_to_proposal` checks the theme belongs to the org; otherwise
  → `NOT_FOUND`.

## Error envelope

MCP errors return as the standard MCP error result:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "isError": true,
  "content": [{ "type": "text", "text": "<message>" }]
}
```

Common messages by category:

### Validation

* `Invalid component tree: <path>: <reason>`
* `section cannot be nested inside another section`
* `embed.src host does not match provider "<provider>"`
* `tree exceeds MAX_NODES (200)`

### Authorization

* `Missing required scope: mcp:use`
* `Proposal not found` — usually means the ID doesn't belong to the user's
  current organization (cross-org access is blocked).

### Concurrency

* `Proposal changed too quickly. Please retry the append.` — 3 consecutive
  optimistic-update conflicts. Refetch via `get_proposal_tree` and retry.

### Out-of-bounds

* `index <N> is out of bounds (must be 0..<max>)`
* `order length <N> does not match document length <M>`
* `order contains duplicate index <N>`

### Theme reference

* `section.section_style "<name>" is not a known slot id in the applied
  theme. Available ids: "soft" (1), "deep" (2), "3".`

## Rate limiting

The MCP server enforces the same per-organization rate limits as the REST
API. Bursts are smoothed; sustained throughput is capped.

If you exceed the limit, the response includes a `Retry-After` hint and the
agent should back off.

## Concurrency model recap

Every mutation tool uses optimistic concurrency on `proposals.updated_at`:

1. Snapshot the doc + `updated_at`.
2. Apply the mutation locally.
3. `UPDATE … WHERE updated_at = <snapshot>` — guard.
4. On conflict (someone else wrote): refresh + retry, up to 3 times.
5. On 3 consecutive conflicts: surface the error to the agent.

Reads (`get_proposal_tree`, `get_proposal_resolved_style`) don't take a
lock — they reflect the current row.

## Best practices

<Steps>
  <Step title="Refetch between mutations">
    Indices shift after `insert` / `update` (1 → N) / `delete` / `reorder`.
    `find_and_replace_in_proposal` is the exception — it doesn't change the
    structure.
  </Step>

  <Step title="Validate before applying when chaining">
    `validate_propal_component_tree` lets you check a tree without writing.
    Useful for dry-runs and error explanation to the user.
  </Step>

  <Step title="Use named section_style ids">
    `section_style: "deep"` survives reordering of the theme's slots. `"2"`
    silently shifts when slots are reordered.
  </Step>

  <Step title="Honor WCAG warnings">
    `generate_theme_from_palette` returns `warnings` — surface them to the
    user before persisting if contrasts are weak.
  </Step>

  <Step title="Surface conflicts gracefully">
    On `Proposal changed too quickly`, refetch and let the agent decide
    (retry, merge, abort).
  </Step>
</Steps>
