# Attach customiser asset (`attach_customiser_asset`)

Binds a previously uploaded object to one asset slot on one record: an option image, an extra choice picture, a font TrueType file, a backboard custom SVG shape, one letter part default upload image, or the customiser storefront product image. Attaching to a slot that already holds a file overwrites it and releases the old file, so the call needs confirm: true. The upload purpose must be the one the slot accepts. It requires the customisers:write scope and an idempotency_key, and supports dry_run.

- Source URL: https://www.signcustomiser.com/help/mcp/tools/attach_customiser_asset/
- Markdown URL: https://www.signcustomiser.com/help/mcp/tools/attach_customiser_asset.md
- MCP endpoint: https://web.signcustomiser.com/mcp
- Required scope: `customisers:write`
- Behaviour: destructive write

## Purpose

Put an uploaded file into one asset slot on one option record, or on the customiser labels document.

## Annotations

| Hint | Value |
| --- | --- |
| readOnlyHint | false |
| destructiveHint | true |
| idempotentHint | true |
| openWorldHint | false |

## Prerequisites

- An upload id from create_upload whose bytes have already been PUT to its presigned target.
- The upload purpose must be the one the slot accepts, which get_customiser_option and this tool description both name.
- The record id, from list_customiser_options; letter_type_id and letter_part for the nested targets.
- A merchant has connected this store and approved the customisers:write scope.

## Side effects

- Moves the staged object into the store permanent files area and writes its URL into the slot.
- Overwrites and releases whatever file the slot already held.
- Consumes the upload: each upload is single use.
- Clears the customiser cached storefront configuration.
- Claims the idempotency_key for at least 24 hours.

## Arguments

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `customiser_id` | integer | yes | The customiser id, from list_customisers. |
| `asset_target` | string | yes | Which asset slot family to write. Closed set. Each value fixes which of the arguments below are needed and which upload purpose the slot accepts. One of: `backboard_image`, `backboard_shape`, `backlight_image`, `colour_image`, `extra_image`, `extra_choice_image`, `font_file`, `letter_type_image`, `letter_part_image`, `letter_part_colour_image`, `material_image`, `mounting_image`, `mounting_colour_image`, `support_finish_image`, `label_product_image`. |
| `option_id` | integer or string | no | The record that owns the slot. An integer for every target except letter_part_image, which is addressed by the part catalogue name. Omitted for label_product_image, which belongs to the customiser rather than to a record. |
| `letter_type_id` | integer | no | The letter type that owns the record, required for letter_part_image and letter_part_colour_image. |
| `letter_part` | string | no | The letter part that owns the colours, required for letter_part_colour_image. For letter_part_image the part is option_id instead. One of: `face`, `back`, `side`, `top`, `bottom`, `left`, `right`, `halo`, `trim`, `tube`. |
| `choice_value` | string | no | One of an image-choice extra's choice values, required for extra_choice_image. |
| `image_slot` | string | no | Which picture of the record to write. Which slots a target offers is target-specific and reported by get_customiser_option; a slot the target does not offer is refused with allowed_values. Omitted for backboard_shape, font_file and label_product_image, whose paths name the slot. Expected to grow. One of: `image`, `button_image`, `example_image`, `texture_image`, `default_upload_image`, `preview_image`. |
| `upload_id` | string | yes | The id of an upload created with this slot purpose, whose bytes have already been sent to its presigned target. Each upload is single use. |
| `confirm` | boolean | no | Must be true to apply the change, because attaching overwrites and releases whatever file the slot already holds. Without it the call returns a confirmation_required error and changes nothing. A dry run does not need it. |
| `idempotency_key` | string | no | A client-generated key unique to this logical write, such as a UUID. Required: repeating the call with the same key and the same arguments replays the original result instead of writing twice, and the same key with different arguments is an idempotency_key_conflict. A dry run claims no key. |
| `dry_run` | boolean | no | When true, the record, the slot and the upload purpose are checked and the upload is not consumed is checked and nothing is written: the result is the same verdict the apply would have reached, and a problem the apply would have raised comes back as the same tool error. Defaults to false. |

## Result

The owning record as it now stands, with the slot URL freshly populated, and the slot that was written. A dry run returns the attach verdict and consumes nothing.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `request_id` | string | yes |  |
| `store` | object | yes |  |
| `store.id` | integer | yes |  |
| `store.name` | string | yes |  |
| `customiser_id` | integer | yes |  |
| `asset_target` | string | yes | One of: `backboard_image`, `backboard_shape`, `backlight_image`, `colour_image`, `extra_image`, `extra_choice_image`, `font_file`, `letter_type_image`, `letter_part_image`, `letter_part_colour_image`, `material_image`, `mounting_image`, `mounting_colour_image`, `support_finish_image`, `label_product_image`. |
| `slot` | string | yes | The slot that was written. For the three targets whose path names the slot, this is shape, font_file or product_image. |
| `resource` | object | no | The owning record as it now stands, with the slot URL populated. Absent on a dry run. |
| `idempotent_replay` | boolean | no | True when this call replayed an earlier attach carrying the same idempotency_key rather than consuming a second upload. |
| `dry_run` | boolean | no | Present and true only when dry_run was requested. |
| `valid` | boolean | no | Present only on a dry run, and always true: a failing dry run returns a tool error instead. |
| `verdict` | object | no | Present only on a dry run: the attach verdict. |

## Error cases

| Code | Recovery |
| --- | --- |
| `confirmation_required` | Send confirm: true. Attaching overwrites whatever the slot already holds. |
| `insufficient_scope` | The connection was approved without customisers:write. |
| `invalid_svg` | The backboard shape failed SVG sanitisation; the problem names the constraint. |
| `missing_idempotency_key` | Send idempotency_key. It is required for this write. |
| `resource_not_found` | No such customiser, record or upload in this store. |
| `upload_already_attached` | Each upload is single use and this one is spent. Create a new upload for this slot. |
| `upload_content_type_mismatch` | What arrived is not the content type that was declared. Create a new upload declaring the real type. |
| `upload_expired` | The staged upload passed its retention window. Start again at create_upload. |
| `upload_object_missing` | No bytes were found at the staging target. PUT the file to the target create_upload returned, then attach. |
| `upload_purpose_mismatch` | The upload was created for a different purpose; allowed_values names the purpose this slot accepts. Create a new upload with it. |
| `upload_size_exceeded` | The object is larger than the purpose allows; the problem reports max_byte_size. |
| `validation_failed` | A slot this target does not offer, or a missing nested key; allowed_values lists what the target accepts. |

## Example: Attach a swatch to a colour

Request:

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "attach_customiser_asset",
    "arguments": {
      "customiser_id": 42,
      "asset_target": "colour_image",
      "option_id": 7,
      "image_slot": "image",
      "upload_id": "upl_9f2c4b7ad13e6058ba41cd",
      "confirm": true,
      "idempotency_key": "d4e5f607-8192-4c0d-9e1f-2a3b4c5d6e7f"
    }
  }
}
```

Response:

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "structuredContent": {
      "request_id": "req_01jz9x2k7c8f3m5n6p7q8r9s0t",
      "store": {
        "id": 12,
        "name": "Demo Signs"
      },
      "customiser_id": 42,
      "asset_target": "colour_image",
      "slot": "image",
      "resource": {
        "id": 7,
        "name": "Warm White",
        "image": "https://cdn.example.com/files/2f1c9c0e.png"
      },
      "idempotent_replay": false
    }
  }
}
```

## Example: The upload was created for another purpose (error)

Request:

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "attach_customiser_asset",
    "arguments": {
      "customiser_id": 42,
      "asset_target": "colour_image",
      "option_id": 7,
      "image_slot": "image",
      "upload_id": "upl_000000000000000000000",
      "confirm": true,
      "idempotency_key": "d4e5f607-8192-4c0d-9e1f-2a3b4c5d6e7f"
    }
  }
}
```

Response:

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "isError": true,
    "structuredContent": {
      "error": {
        "code": "upload_purpose_mismatch",
        "title": "Upload purpose mismatch",
        "detail": "This slot accepts uploads created with purpose option_image, and this upload was created with purpose font_ttf. Create a new upload with the slot's purpose.",
        "recovery": "The upload was created for a different purpose. Start again at create_upload with the purpose this attachment needs.",
        "pointer": "/upload_id",
        "allowed_values": [
          "option_image"
        ]
      }
    }
  }
}
```
