# MCP tools

Every tool the store MCP server at `https://api.shopdeskbd.com/ext/mcp/v1` offers — 36 tools, 18 read-only and 18 writes — generated from the [tool manifest](https://www.shopdeskbd.com/openapi/mcp-tools-v1.json), which the server's own tool registry produces. Authentication: Authorization: Bearer <API key> — the same keys and scopes as the REST API. Scope-filtered: a key lists only the tools all of whose scopes it holds. See [Build with AI](https://www.shopdeskbd.com/developers/ai.md) for setup.

A full-scope key lists at most 50 tools. Read tools count against the key's read limit (report and summary tools as several reads); write tools, and anything unrecognised, count against the write limit — see [Rate limits](https://www.shopdeskbd.com/developers/rate-limits.md).

`create_product`, `create_category`, `create_brand`, `add_product_images_from_urls`, `attach_product_video` declare `openWorldHint: true`: they download the image links they are given, through the same checks and limits as the REST API — see [Photos by link](https://www.shopdeskbd.com/developers/guide-photos-by-link.md). Every other tool declares `false`. When a name a tool was given matches nothing or several rows, the error object carries `candidates` beside `fields`.

## Read tools

### list_products

A cursor page of products with their variants nested — the variant is the sellable unit (SKU, price). Filters: `status`, `category_id` (UUID), `search` (product name or variant SKU), `updated_after` (ISO 8601) to sync only what changed. Use `get_product` instead when you already hold an id. Returns `{ data: [...], meta: { has_more, next_cursor } }`; pass `meta.next_cursor` as `starting_after` to fetch the next page.

- **Title:** List products
- **Kind:** Read-only
- **openWorldHint:** `false`
- **Scopes:** `products:read`
- **Idempotency key:** not taken
- **Metering:** 1 read per call
- **REST twin:** `GET /ext/v1/products`

| Argument | Type | Description |
| --- | --- | --- |
| `limit` | integer | Page size, 1–100 (default 25). |
| `starting_after` | uuid | Cursor: the previous page's `meta.next_cursor`. Loop until `meta.next_cursor` is null. Do not combine with `ending_before`. |
| `ending_before` | uuid | Cursor for paging backwards: the id of your current first row. |
| `sort` | "created_at" \| "-created_at" | `-created_at` (newest first, the default) or `created_at`. Use `updated_after` — not sort — to sync changes. |
| `store_id` | uuid | Restrict to one store (UUID). Breadth control for company-scoped keys; a store-scoped key may omit it (when sent it must match the key's own store). Narrows the tenant fence, never widens it. |
| `status` | "draft" \| "active" \| "archived" | — |
| `category_id` | uuid | Only products in this category. |
| `updated_after` | string | ISO 8601 instant. Only rows changed since then — THE sync filter: it catches rows created earlier but modified since your last poll. |
| `search` | string | Match on product name or variant SKU. |

Returns `structuredContent` with `data`, `meta` (full JSON Schema in the [tool manifest](https://www.shopdeskbd.com/openapi/mcp-tools-v1.json)), and the same value as minified JSON text.

### get_product

One product by id (UUID), variants nested. An id outside this key's stores is a plain not-found — indistinguishable from a nonexistent one.

- **Title:** Get a product
- **Kind:** Read-only
- **openWorldHint:** `false`
- **Scopes:** `products:read`
- **Idempotency key:** not taken
- **Metering:** 1 read per call
- **REST twin:** `GET /ext/v1/products/{id}`

| Argument | Type | Description |
| --- | --- | --- |
| `id` (required) | uuid | The product id. |
| `store_id` | uuid | Restrict to one store (UUID). Breadth control for company-scoped keys; a store-scoped key may omit it (when sent it must match the key's own store). Narrows the tenant fence, never widens it. |

Returns `structuredContent` with `id`, `store_id`, `name`, `slug`, `short_description`, `description`, `thumbnail`, `status`, `delivery_charge`, `rating_average`, `rating_count`, `brand`, `categories`, `images`, `variants`, `videos`, `created_at`, `updated_at` (full JSON Schema in the [tool manifest](https://www.shopdeskbd.com/openapi/mcp-tools-v1.json)), and the same value as minified JSON text.

### list_categories

A cursor page of product categories (id, name, slug, status). `search` matches name or slug — how a category name resolves to its id. Category ids feed `list_products`’ `category_id` filter. Returns `{ data: [...], meta: { has_more, next_cursor } }`; pass `meta.next_cursor` as `starting_after` to fetch the next page.

- **Title:** List categories
- **Kind:** Read-only
- **openWorldHint:** `false`
- **Scopes:** `products:read`
- **Idempotency key:** not taken
- **Metering:** 1 read per call
- **REST twin:** `GET /ext/v1/categories`

| Argument | Type | Description |
| --- | --- | --- |
| `limit` | integer | Page size, 1–100 (default 25). |
| `starting_after` | uuid | Cursor: the previous page's `meta.next_cursor`. Loop until `meta.next_cursor` is null. Do not combine with `ending_before`. |
| `ending_before` | uuid | Cursor for paging backwards: the id of your current first row. |
| `sort` | "created_at" \| "-created_at" | `-created_at` (newest first, the default) or `created_at`. Use `updated_after` — not sort — to sync changes. |
| `store_id` | uuid | Restrict to one store (UUID). Breadth control for company-scoped keys; a store-scoped key may omit it (when sent it must match the key's own store). Narrows the tenant fence, never widens it. |
| `status` | "active" \| "inactive" | — |
| `search` | string | Match on name or slug (partial, case-insensitive). |

Returns `structuredContent` with `data`, `meta` (full JSON Schema in the [tool manifest](https://www.shopdeskbd.com/openapi/mcp-tools-v1.json)), and the same value as minified JSON text.

### list_brands

A cursor page of brands (id, name, slug, status). `search` matches name or slug. Returns `{ data: [...], meta: { has_more, next_cursor } }`; pass `meta.next_cursor` as `starting_after` to fetch the next page.

- **Title:** List brands
- **Kind:** Read-only
- **openWorldHint:** `false`
- **Scopes:** `products:read`
- **Idempotency key:** not taken
- **Metering:** 1 read per call
- **REST twin:** `GET /ext/v1/brands`

| Argument | Type | Description |
| --- | --- | --- |
| `limit` | integer | Page size, 1–100 (default 25). |
| `starting_after` | uuid | Cursor: the previous page's `meta.next_cursor`. Loop until `meta.next_cursor` is null. Do not combine with `ending_before`. |
| `ending_before` | uuid | Cursor for paging backwards: the id of your current first row. |
| `sort` | "created_at" \| "-created_at" | `-created_at` (newest first, the default) or `created_at`. Use `updated_after` — not sort — to sync changes. |
| `store_id` | uuid | Restrict to one store (UUID). Breadth control for company-scoped keys; a store-scoped key may omit it (when sent it must match the key's own store). Narrows the tenant fence, never widens it. |
| `status` | "active" \| "inactive" | — |
| `search` | string | Match on name or slug (partial, case-insensitive). |

Returns `structuredContent` with `data`, `meta` (full JSON Schema in the [tool manifest](https://www.shopdeskbd.com/openapi/mcp-tools-v1.json)), and the same value as minified JSON text.

### list_orders

A cursor page of orders, compact by default — pass `expand: ['customer','items']` for the full objects (keep pages compact when walking many). Filters: `status`, `payment_status`, `fulfillment_status`, `channel` (channel SLUG from `list_channels`, not an id), `created_after`/`created_before`, `search` (order number, customer name or phone — any Bangladeshi phone form), `customer_id`, `assignee_id`, `follow_up_before`/`follow_up_after` (the call-back schedule) and `updated_after` (ISO 8601) — THE sync filter: it catches an order placed last month but confirmed this morning, which a `created_after` poll would miss. For a phone lookup or the call list, `find_orders` and `list_orders_needing_attention` answer in one call. Returns `{ data: [...], meta: { has_more, next_cursor } }`; pass `meta.next_cursor` as `starting_after` to fetch the next page.

- **Title:** List orders
- **Kind:** Read-only
- **openWorldHint:** `false`
- **Scopes:** `orders:read`
- **Idempotency key:** not taken
- **Metering:** 1 read per call
- **REST twin:** `GET /ext/v1/orders`

| Argument | Type | Description |
| --- | --- | --- |
| `limit` | integer | Page size, 1–100 (default 25). |
| `starting_after` | uuid | Cursor: the previous page's `meta.next_cursor`. Loop until `meta.next_cursor` is null. Do not combine with `ending_before`. |
| `ending_before` | uuid | Cursor for paging backwards: the id of your current first row. |
| `sort` | "created_at" \| "-created_at" | `-created_at` (newest first, the default) or `created_at`. Use `updated_after` — not sort — to sync changes. |
| `store_id` | uuid | Restrict to one store (UUID). Breadth control for company-scoped keys; a store-scoped key may omit it (when sent it must match the key's own store). Narrows the tenant fence, never widens it. |
| `status` | "pending" \| "follow_up" \| "confirmed" \| "processing" \| "ready_to_ship" \| "in_transit" \| "delivered" \| "returned" \| "failed_delivery" \| "cancelled" \| "hold" \| "spam" | — |
| `payment_status` | "unpaid" \| "partially_paid" \| "paid" \| "refunded" | — |
| `fulfillment_status` | "unfulfilled" \| "partially_fulfilled" \| "fulfilled" \| "returned" | — |
| `channel` | string | Sales channel slug (e.g. `facebook`), not its id. |
| `created_after` | string | ISO 8601 instant. |
| `created_before` | string | ISO 8601 instant. |
| `updated_after` | string | ISO 8601 instant. Only rows changed since then — THE sync filter: it catches rows created earlier but modified since your last poll. |
| `search` | string | Match on order number, customer name or phone. |
| `customer_id` | uuid | Only this customer's orders. |
| `assignee_id` | uuid | Only orders assigned to this staff member. |
| `follow_up_before` | string | ISO 8601. Only orders with a call-back due before it. |
| `follow_up_after` | string | ISO 8601. Only orders with a call-back due after it. |
| `expand` | "customer" \| "items"[] | Related objects to include: `customer`, `items`. |

Returns `structuredContent` with `data`, `meta` (full JSON Schema in the [tool manifest](https://www.shopdeskbd.com/openapi/mcp-tools-v1.json)), and the same value as minified JSON text.

### get_order

One order by id (UUID). Pass `expand: ["customer","items"]` for the customer and line items; payment ROWS live behind `list_order_payments` (a separate scope).

- **Title:** Get an order
- **Kind:** Read-only
- **openWorldHint:** `false`
- **Scopes:** `orders:read`
- **Idempotency key:** not taken
- **Metering:** 1 read per call
- **REST twin:** `GET /ext/v1/orders/{id}`

| Argument | Type | Description |
| --- | --- | --- |
| `id` (required) | uuid | The order id. |
| `expand` | "customer" \| "items"[] | Related objects to include: `customer`, `items`. |
| `store_id` | uuid | Restrict to one store (UUID). Breadth control for company-scoped keys; a store-scoped key may omit it (when sent it must match the key's own store). Narrows the tenant fence, never widens it. |

Returns `structuredContent` with `id`, `store_id`, `number`, `source`, `channel`, `customer_id`, `location_id`, `status`, `payment_status`, `fulfillment_status`, `subtotal`, `discount_amount`, `shipping_charge`, `tax_amount`, `total_amount`, `paid_amount`, `due_amount`, `refunded_amount`, `shipping_address`, `coupon`, `custom_fields`, `note`, `follow_up_at`, `follow_up_note`, `assignee`, `delivered_at`, `created_at`, `updated_at`, `items`, `customer` (full JSON Schema in the [tool manifest](https://www.shopdeskbd.com/openapi/mcp-tools-v1.json)), and the same value as minified JSON text.

### list_order_payments

Every payment recorded against one order, oldest first: settled, pending and refunded rows (refunds are negative amounts with status `refunded`). Small and unpaginated. Requires the `payments:read` scope — `orders:read` alone is not enough.

- **Title:** List an order's payments
- **Kind:** Read-only
- **openWorldHint:** `false`
- **Scopes:** `payments:read`
- **Idempotency key:** not taken
- **Metering:** 1 read per call
- **REST twin:** `GET /ext/v1/orders/{id}/payments`

| Argument | Type | Description |
| --- | --- | --- |
| `order_id` (required) | uuid | The order id. |
| `store_id` | uuid | Restrict to one store (UUID). Breadth control for company-scoped keys; a store-scoped key may omit it (when sent it must match the key's own store). Narrows the tenant fence, never widens it. |

Returns `structuredContent` with `data` (full JSON Schema in the [tool manifest](https://www.shopdeskbd.com/openapi/mcp-tools-v1.json)), and the same value as minified JSON text.

### list_customers

A cursor page of customer records (name, phone, email, status). `search` matches name, email or phone (any Bangladeshi phone form). Use `get_customer` for one record with its addresses, or `get_customer_summary` for a customer with their order history. Returns `{ data: [...], meta: { has_more, next_cursor } }`; pass `meta.next_cursor` as `starting_after` to fetch the next page.

- **Title:** List customers
- **Kind:** Read-only
- **openWorldHint:** `false`
- **Scopes:** `customers:read`
- **Idempotency key:** not taken
- **Metering:** 1 read per call
- **REST twin:** `GET /ext/v1/customers`

| Argument | Type | Description |
| --- | --- | --- |
| `limit` | integer | Page size, 1–100 (default 25). |
| `starting_after` | uuid | Cursor: the previous page's `meta.next_cursor`. Loop until `meta.next_cursor` is null. Do not combine with `ending_before`. |
| `ending_before` | uuid | Cursor for paging backwards: the id of your current first row. |
| `sort` | "created_at" \| "-created_at" | `-created_at` (newest first, the default) or `created_at`. Use `updated_after` — not sort — to sync changes. |
| `store_id` | uuid | Restrict to one store (UUID). Breadth control for company-scoped keys; a store-scoped key may omit it (when sent it must match the key's own store). Narrows the tenant fence, never widens it. |
| `search` | string | Match on name, email or phone. |

Returns `structuredContent` with `data`, `meta` (full JSON Schema in the [tool manifest](https://www.shopdeskbd.com/openapi/mcp-tools-v1.json)), and the same value as minified JSON text.

### get_customer

One customer by id (UUID), addresses included — the detail shape, a superset of the `list_customers` rows. `expand: ["stats"]` adds their order history in four numbers (needs `orders:read` too; omitted without it).

- **Title:** Get a customer
- **Kind:** Read-only
- **openWorldHint:** `false`
- **Scopes:** `customers:read`
- **Idempotency key:** not taken
- **Metering:** 1 read per call
- **REST twin:** `GET /ext/v1/customers/{id}`

| Argument | Type | Description |
| --- | --- | --- |
| `id` (required) | uuid | The customer id. |
| `expand` | "stats"[] | `stats`: order count, total spent, last order, returns. |
| `store_id` | uuid | Restrict to one store (UUID). Breadth control for company-scoped keys; a store-scoped key may omit it (when sent it must match the key's own store). Narrows the tenant fence, never widens it. |

Returns `structuredContent` with `id`, `store_id`, `full_name`, `email`, `phone`, `status`, `type`, `gender`, `created_at`, `updated_at`, `addresses`, `stats` (full JSON Schema in the [tool manifest](https://www.shopdeskbd.com/openapi/mcp-tools-v1.json)), and the same value as minified JSON text.

### get_inventory_levels

A cursor page of stock levels, one row per variant per location. Filters: `warehouse_id`, `variant_id` (both UUIDs), and `low_stock: true` for only the rows at or below their variant’s low-stock threshold — the reorder question answered server-side. `missing_only: true` (needs `warehouse_id`) instead lists the variants that have NEVER had stock set at that location — rows with null `id` and `status: "stock_not_set"`; use it to find what is left to stock. Returns `{ data: [...], meta: { has_more, next_cursor } }`; pass `meta.next_cursor` as `starting_after` to fetch the next page.

- **Title:** Get inventory levels
- **Kind:** Read-only
- **openWorldHint:** `false`
- **Scopes:** `inventory:read`
- **Idempotency key:** not taken
- **Metering:** 1 read per call
- **REST twin:** `GET /ext/v1/inventory/levels`

| Argument | Type | Description |
| --- | --- | --- |
| `limit` | integer | Page size, 1–100 (default 25). |
| `starting_after` | uuid | Cursor: the previous page's `meta.next_cursor`. Loop until `meta.next_cursor` is null. Do not combine with `ending_before`. |
| `ending_before` | uuid | Cursor for paging backwards: the id of your current first row. |
| `sort` | "created_at" \| "-created_at" | `-created_at` (newest first, the default) or `created_at`. Use `updated_after` — not sort — to sync changes. |
| `store_id` | uuid | Restrict to one store (UUID). Breadth control for company-scoped keys; a store-scoped key may omit it (when sent it must match the key's own store). Narrows the tenant fence, never widens it. |
| `warehouse_id` | uuid | Only levels at this location. |
| `variant_id` | uuid | Only levels for this variant. |
| `low_stock` | boolean | Only rows at/below the low-stock threshold. |
| `missing_only` | boolean | Only variants with no stock level at `warehouse_id` yet. Requires `warehouse_id`; not combinable with `low_stock`. |

Returns `structuredContent` with `data`, `meta` (full JSON Schema in the [tool manifest](https://www.shopdeskbd.com/openapi/mcp-tools-v1.json)), and the same value as minified JSON text.

### list_locations

A cursor page of warehouses and other stock locations. These are COMPANY-scoped: both key types see the same set and `store_id` does not narrow them. Location ids feed `get_inventory_levels`’ `warehouse_id` filter. Returns `{ data: [...], meta: { has_more, next_cursor } }`; pass `meta.next_cursor` as `starting_after` to fetch the next page.

- **Title:** List locations
- **Kind:** Read-only
- **openWorldHint:** `false`
- **Scopes:** `locations:read`
- **Idempotency key:** not taken
- **Metering:** 1 read per call
- **REST twin:** `GET /ext/v1/locations`

| Argument | Type | Description |
| --- | --- | --- |
| `limit` | integer | Page size, 1–100 (default 25). |
| `starting_after` | uuid | Cursor: the previous page's `meta.next_cursor`. Loop until `meta.next_cursor` is null. Do not combine with `ending_before`. |
| `ending_before` | uuid | Cursor for paging backwards: the id of your current first row. |
| `sort` | "created_at" \| "-created_at" | `-created_at` (newest first, the default) or `created_at`. Use `updated_after` — not sort — to sync changes. |
| `store_id` | uuid | Restrict to one store (UUID). Breadth control for company-scoped keys; a store-scoped key may omit it (when sent it must match the key's own store). Narrows the tenant fence, never widens it. |

Returns `structuredContent` with `data`, `meta` (full JSON Schema in the [tool manifest](https://www.shopdeskbd.com/openapi/mcp-tools-v1.json)), and the same value as minified JSON text.

### list_channels

A cursor page of sales channels (id, name, slug) — order attribution metadata. The SLUG is what `list_orders`’ `channel` filter takes. Returns `{ data: [...], meta: { has_more, next_cursor } }`; pass `meta.next_cursor` as `starting_after` to fetch the next page.

- **Title:** List sales channels
- **Kind:** Read-only
- **openWorldHint:** `false`
- **Scopes:** `channels:read`
- **Idempotency key:** not taken
- **Metering:** 1 read per call
- **REST twin:** `GET /ext/v1/channels`

| Argument | Type | Description |
| --- | --- | --- |
| `limit` | integer | Page size, 1–100 (default 25). |
| `starting_after` | uuid | Cursor: the previous page's `meta.next_cursor`. Loop until `meta.next_cursor` is null. Do not combine with `ending_before`. |
| `ending_before` | uuid | Cursor for paging backwards: the id of your current first row. |
| `sort` | "created_at" \| "-created_at" | `-created_at` (newest first, the default) or `created_at`. Use `updated_after` — not sort — to sync changes. |
| `store_id` | uuid | Restrict to one store (UUID). Breadth control for company-scoped keys; a store-scoped key may omit it (when sent it must match the key's own store). Narrows the tenant fence, never widens it. |

Returns `structuredContent` with `data`, `meta` (full JSON Schema in the [tool manifest](https://www.shopdeskbd.com/openapi/mcp-tools-v1.json)), and the same value as minified JSON text.

### get_store_profile

Who this connection works for: the store (name, currency, timezone, contact, languages, advance-payment plans) and this key's access (company, the stores it reaches, its scopes and per-minute limits). A company-wide key must pass `store_id` to get a store; without it `store` is null and `access.stores` lists the stores to choose from.

- **Title:** Get the store profile
- **Kind:** Read-only
- **openWorldHint:** `false`
- **Scopes:** `store:read`
- **Idempotency key:** not taken
- **Metering:** 1 read per call
- **REST twin:** `GET /ext/v1/store`, `GET /ext/v1/me`

| Argument | Type | Description |
| --- | --- | --- |
| `store_id` | uuid | Restrict to one store (UUID). Breadth control for company-scoped keys; a store-scoped key may omit it (when sent it must match the key's own store). Narrows the tenant fence, never widens it. |

Returns `structuredContent` with `store`, `access` (full JSON Schema in the [tool manifest](https://www.shopdeskbd.com/openapi/mcp-tools-v1.json)), and the same value as minified JSON text.

### get_sales_summary

Sales for one store over a window of store-local days: orders, net sales, net revenue and average order value, money received, and the cash on delivery still to collect. The figures run on three clocks, stated in `basis`: sales count orders DELIVERED in the window, `money_received` counts payments settled in the window, and `cod_outstanding` is a balance as of today. Name the window with `period` or `from`/`to` (default: this month so far). `compare_to: "previous_period"` adds the same figures for the window just before (plans with Advanced Reports only). A company-wide key must pass `store_id`.

- **Title:** Get a sales summary
- **Kind:** Read-only
- **openWorldHint:** `false`
- **Scopes:** `reports:read`
- **Idempotency key:** not taken
- **Metering:** 5 reads per call
- **response_format:** `concise` or `detailed` (default `concise`)
- **REST twin:** `GET /ext/v1/reports/sales-summary`

| Argument | Type | Description |
| --- | --- | --- |
| `period` | "today" \| "yesterday" \| "last_7_days" \| "last_30_days" \| "this_month" \| "last_month" | A named window on the store calendar. |
| `from` | string | First day, YYYY-MM-DD (inclusive). Not with `period`. |
| `to` | string | Last day, YYYY-MM-DD (inclusive). At most 366 days. |
| `compare_to` | "previous_period" | Adds `previous_period`. |
| `channel` | string | Only this sales channel (its slug, see `list_channels`). Money received and COD to collect are then null: payments are not split by channel. |
| `store_id` | uuid | Restrict to one store (UUID). Breadth control for company-scoped keys; a store-scoped key may omit it (when sent it must match the key's own store). Narrows the tenant fence, never widens it. |
| `response_format` | "concise" \| "detailed" | `concise` (default): the fields needed to act, names beside ids. `detailed`: the full objects, as the REST API returns them. |

Returns `structuredContent` with `from`, `to`, `orders`, `gross_sales`, `net_sales`, `net_revenue`, `average_order_value`, `money_received`, `store_id`, `time_zone`, `channel`, `basis`, `cod_outstanding`, `previous_period` (full JSON Schema in the [tool manifest](https://www.shopdeskbd.com/openapi/mcp-tools-v1.json)), and the same value as minified JSON text.

### list_low_stock

What needs restocking: stock rows at or below their low-stock threshold, with product and variant names, on-hand, reserved and available units. Optionally one location (`location_id`, see `list_locations`). Pages like every list: pass `meta.next_cursor` as `starting_after` for more.

- **Title:** List low-stock items
- **Kind:** Read-only
- **openWorldHint:** `false`
- **Scopes:** `inventory:read`
- **Idempotency key:** not taken
- **Metering:** 1 read per call
- **response_format:** `concise` or `detailed` (default `concise`)
- **REST twin:** `GET /ext/v1/inventory/levels`

| Argument | Type | Description |
| --- | --- | --- |
| `location_id` | uuid | Only this location (warehouse). |
| `limit` | integer | Page size, 1–100 (default 25). |
| `starting_after` | uuid | The previous page's `meta.next_cursor`, to fetch the next page. Stop when it is null. |
| `store_id` | uuid | Restrict to one store (UUID). Breadth control for company-scoped keys; a store-scoped key may omit it (when sent it must match the key's own store). Narrows the tenant fence, never widens it. |
| `response_format` | "concise" \| "detailed" | `concise` (default): the fields needed to act, names beside ids. `detailed`: the full objects, as the REST API returns them. |

Returns `structuredContent` with `data`, `meta` (full JSON Schema in the [tool manifest](https://www.shopdeskbd.com/openapi/mcp-tools-v1.json)), and the same value as minified JSON text.

### find_orders

Finds orders by an order number, a customer phone number or a customer name, newest first. A phone matches in any Bangladeshi form (+8801…, 8801…, 01…). Each row carries the customer, the lines, what is due and the call-back state. Optionally narrowed by `status`.

- **Title:** Find orders
- **Kind:** Read-only
- **openWorldHint:** `false`
- **Scopes:** `orders:read`
- **Idempotency key:** not taken
- **Metering:** 1 read per call
- **response_format:** `concise` or `detailed` (default `concise`)
- **REST twin:** `GET /ext/v1/orders`

| Argument | Type | Description |
| --- | --- | --- |
| `query` (required) | string | An order number, a phone number or a customer name. |
| `status` | "pending" \| "follow_up" \| "confirmed" \| "processing" \| "ready_to_ship" \| "in_transit" \| "delivered" \| "returned" \| "failed_delivery" \| "cancelled" \| "hold" \| "spam" | — |
| `limit` | integer | Page size, 1–100 (default 25). |
| `starting_after` | uuid | The previous page's `meta.next_cursor`, to fetch the next page. Stop when it is null. |
| `store_id` | uuid | Restrict to one store (UUID). Breadth control for company-scoped keys; a store-scoped key may omit it (when sent it must match the key's own store). Narrows the tenant fence, never widens it. |
| `response_format` | "concise" \| "detailed" | `concise` (default): the fields needed to act, names beside ids. `detailed`: the full objects, as the REST API returns them. |

Returns `structuredContent` with `data`, `meta` (full JSON Schema in the [tool manifest](https://www.shopdeskbd.com/openapi/mcp-tools-v1.json)), and the same value as minified JSON text.

### list_orders_needing_attention

A work queue, oldest first. `unconfirmed`: new orders still `pending` — the confirmation-call list. `follow_up_due`: orders in `follow_up` whose call-back time has passed — the same queue the dashboard's overdue follow-ups count. Record each call with `record_call_outcome`.

- **Title:** List orders needing attention
- **Kind:** Read-only
- **openWorldHint:** `false`
- **Scopes:** `orders:read`
- **Idempotency key:** not taken
- **Metering:** 1 read per call
- **response_format:** `concise` or `detailed` (default `concise`)
- **REST twin:** `GET /ext/v1/orders`

| Argument | Type | Description |
| --- | --- | --- |
| `kind` (required) | "unconfirmed" \| "follow_up_due" | Which queue. |
| `limit` | integer | Page size, 1–100 (default 25). |
| `starting_after` | uuid | The previous page's `meta.next_cursor`, to fetch the next page. Stop when it is null. |
| `store_id` | uuid | Restrict to one store (UUID). Breadth control for company-scoped keys; a store-scoped key may omit it (when sent it must match the key's own store). Narrows the tenant fence, never widens it. |
| `response_format` | "concise" \| "detailed" | `concise` (default): the fields needed to act, names beside ids. `detailed`: the full objects, as the REST API returns them. |

Returns `structuredContent` with `data`, `meta` (full JSON Schema in the [tool manifest](https://www.shopdeskbd.com/openapi/mcp-tools-v1.json)), and the same value as minified JSON text.

### get_customer_summary

One customer and their history: order count, total spent on delivered orders, last order, returns, and their latest 5 orders. Name them by `customer_id` or by `phone` (any Bangladeshi form). When a phone matches more than one customer (a company-wide key sees several stores), `customer` is null and `candidates` lists them — call again with a `customer_id`.

- **Title:** Get a customer's history
- **Kind:** Read-only
- **openWorldHint:** `false`
- **Scopes:** `customers:read` + `orders:read`
- **Idempotency key:** not taken
- **Metering:** 5 reads per call
- **response_format:** `concise` or `detailed` (default `concise`)
- **REST twin:** `GET /ext/v1/customers`, `GET /ext/v1/customers/{id}`, `GET /ext/v1/orders`

| Argument | Type | Description |
| --- | --- | --- |
| `customer_id` | uuid | The customer id. |
| `phone` | string | A Bangladeshi mobile number. Or use `customer_id`. |
| `store_id` | uuid | Restrict to one store (UUID). Breadth control for company-scoped keys; a store-scoped key may omit it (when sent it must match the key's own store). Narrows the tenant fence, never widens it. |
| `response_format` | "concise" \| "detailed" | `concise` (default): the fields needed to act, names beside ids. `detailed`: the full objects, as the REST API returns them. |

Returns `structuredContent` with `customer`, `recent_orders`, `candidates` (full JSON Schema in the [tool manifest](https://www.shopdeskbd.com/openapi/mcp-tools-v1.json)), and the same value as minified JSON text.

## Write tools

### create_order

Places an order through the same engine as the merchant’s own staff: stock is reserved, totals are computed server-side, and any coupon is priced by the real coupon engine (an unusable code fails the order rather than mispricing it). Identify the buyer with `customer_id` OR an inline `customer` (find-or-create by phone) — never both. Each line needs `variant_id` or `sku`, plus a positive `quantity`. Returns the created order, the same shape `get_order` returns.

- **Title:** Create an order
- **Kind:** Write (adds data; safe to repeat)
- **openWorldHint:** `false`
- **Scopes:** `orders:write`
- **Idempotency key:** required
- **Metering:** 1 write per call
- **REST twin:** `POST /ext/v1/orders`

| Argument | Type | Description |
| --- | --- | --- |
| `idempotency_key` (required) | string | A value unique to this LOGICAL operation (a UUID is ideal) — reuse the SAME value when retrying it, so a lost response can never double-run the write. Required here. |
| `store_id` | uuid | Which store to write into (UUID). REQUIRED for a company-scoped key; a store-scoped key may omit it (when sent it must match the key's own store). |
| `customer_id` | uuid | An existing customer. Provide this OR `customer`. |
| `customer` | object | Find-or-create by phone. Provide this OR `customer_id`. |
| `items` (required) | object[] | The order lines — at least one. |
| `channel` | string | Sales-channel SLUG for attribution (see `list_channels`); defaults to the store’s `manual` channel. |
| `location_id` | uuid | Warehouse to allocate stock from; defaults to the company default location. |
| `shipping_address` | object | Free-form address object, stored verbatim. |
| `shipping_charge` | number | Delivery charge as your system computed it; default 0. |
| `tax_amount` | number | — |
| `coupon_code` | string | — |
| `note` | string | — |
| `payment` | object | An upfront settled payment; omit for an unpaid order and record later payments with `record_order_payment`. |

Returns `structuredContent` with `id`, `store_id`, `number`, `source`, `channel`, `customer_id`, `location_id`, `status`, `payment_status`, `fulfillment_status`, `subtotal`, `discount_amount`, `shipping_charge`, `tax_amount`, `total_amount`, `paid_amount`, `due_amount`, `refunded_amount`, `shipping_address`, `coupon`, `custom_fields`, `note`, `follow_up_at`, `follow_up_note`, `assignee`, `delivered_at`, `created_at`, `updated_at`, `items`, `customer` (full JSON Schema in the [tool manifest](https://www.shopdeskbd.com/openapi/mcp-tools-v1.json)), and the same value as minified JSON text.

### update_order_status

Moves one order to a new status under the SAME strict transition map the merchant’s staff use — an illegal move is refused as `INVALID_STATUS_TRANSITION`. A `delivered` order cannot be moved to `returned`: returns of delivered orders are made in the dashboard. A `returned` order keeps its stock held (not for sale) until the parcel is received in the dashboard; on a never-delivered order it keeps up to the delivery charge from an advance only when the parcel's pickup is recorded, else everything paid is owed back and no courier charge is booked. `returned` is refused as `CONFLICT` while a courier that reports pickups has not reported picking the parcel up; `in_transit` is refused while the order’s parcel is cancelled (book it again in the dashboard). `note` lands on the order’s timeline. Use `cancel_order` for cancellation. Returns the updated order.

- **Title:** Update an order’s status
- **Kind:** Write (changes existing data)
- **openWorldHint:** `false`
- **Scopes:** `orders:write`
- **Idempotency key:** honoured
- **Metering:** 1 write per call
- **REST twin:** `PATCH /ext/v1/orders/{id}/status`

| Argument | Type | Description |
| --- | --- | --- |
| `id` (required) | uuid | The order id. |
| `status` (required) | "pending" \| "follow_up" \| "confirmed" \| "processing" \| "ready_to_ship" \| "in_transit" \| "delivered" \| "returned" \| "failed_delivery" \| "cancelled" \| "hold" \| "spam" | The target status. |
| `note` | string | — |
| `idempotency_key` | string | A value unique to this LOGICAL operation (a UUID is ideal) — reuse the SAME value when retrying it, so a lost response can never double-run the write. Optional here, but recommended for automated callers. |
| `store_id` | uuid | Which store to write into (UUID). REQUIRED for a company-scoped key; a store-scoped key may omit it (when sent it must match the key's own store). |

Returns `structuredContent` with `id`, `store_id`, `number`, `source`, `channel`, `customer_id`, `location_id`, `status`, `payment_status`, `fulfillment_status`, `subtotal`, `discount_amount`, `shipping_charge`, `tax_amount`, `total_amount`, `paid_amount`, `due_amount`, `refunded_amount`, `shipping_address`, `coupon`, `custom_fields`, `note`, `follow_up_at`, `follow_up_note`, `assignee`, `delivered_at`, `created_at`, `updated_at`, `items`, `customer` (full JSON Schema in the [tool manifest](https://www.shopdeskbd.com/openapi/mcp-tools-v1.json)), and the same value as minified JSON text.

### cancel_order

Cancels one order — sugar for the `cancelled` transition, so the same transition map decides whether THIS order can still be cancelled. Reserved stock is released by the same lifecycle rules as the desk. Returns the cancelled order.

- **Title:** Cancel an order
- **Kind:** Write (changes existing data)
- **openWorldHint:** `false`
- **Scopes:** `orders:write`
- **Idempotency key:** honoured
- **Metering:** 1 write per call
- **REST twin:** `POST /ext/v1/orders/{id}/cancel`

| Argument | Type | Description |
| --- | --- | --- |
| `id` (required) | uuid | The order id. |
| `note` | string | Recorded on the order’s timeline. |
| `idempotency_key` | string | A value unique to this LOGICAL operation (a UUID is ideal) — reuse the SAME value when retrying it, so a lost response can never double-run the write. Optional here, but recommended for automated callers. |
| `store_id` | uuid | Which store to write into (UUID). REQUIRED for a company-scoped key; a store-scoped key may omit it (when sent it must match the key's own store). |

Returns `structuredContent` with `id`, `store_id`, `number`, `source`, `channel`, `customer_id`, `location_id`, `status`, `payment_status`, `fulfillment_status`, `subtotal`, `discount_amount`, `shipping_charge`, `tax_amount`, `total_amount`, `paid_amount`, `due_amount`, `refunded_amount`, `shipping_address`, `coupon`, `custom_fields`, `note`, `follow_up_at`, `follow_up_note`, `assignee`, `delivered_at`, `created_at`, `updated_at`, `items`, `customer` (full JSON Schema in the [tool manifest](https://www.shopdeskbd.com/openapi/mcp-tools-v1.json)), and the same value as minified JSON text.

### record_order_payment

Records a payment that has ALREADY been received — it moves no money itself and contacts no payment provider. The staff payment path verbatim: the amount is capped at the order’s net due (`payment_exceeds_due` otherwise), and recording the same `transaction_id` twice in a store is a conflict (`duplicate_transaction_id`); a method set to require a transaction id refuses a payment without one (`transaction_id_required`). When a courier has already collected this order at the door, the collection is posted automatically and the order owes nothing — a payment above the due is then refused as `cod_already_collected`, naming the courier account the cash is sitting in; do not retry it, and record the courier’s remittance instead when it reaches the bank. Requires the `payments:write` scope — `orders:write` alone is not enough. Returns the updated order with its payment rollup.

- **Title:** Record a payment on an order
- **Kind:** Write (adds data; safe to repeat)
- **openWorldHint:** `false`
- **Scopes:** `payments:write`
- **Idempotency key:** required
- **Metering:** 1 write per call
- **REST twin:** `POST /ext/v1/orders/{id}/payments`

| Argument | Type | Description |
| --- | --- | --- |
| `order_id` (required) | uuid | The order id. |
| `amount` (required) | number | Settled amount — above 0. |
| `payment_method_id` | uuid | One of the store’s ACTIVE payment methods. |
| `transaction_id` | string | Provider transaction id — unique per store. |
| `idempotency_key` (required) | string | A value unique to this LOGICAL operation (a UUID is ideal) — reuse the SAME value when retrying it, so a lost response can never double-run the write. Required here. |
| `store_id` | uuid | Which store to write into (UUID). REQUIRED for a company-scoped key; a store-scoped key may omit it (when sent it must match the key's own store). |

Returns `structuredContent` with `id`, `store_id`, `number`, `source`, `channel`, `customer_id`, `location_id`, `status`, `payment_status`, `fulfillment_status`, `subtotal`, `discount_amount`, `shipping_charge`, `tax_amount`, `total_amount`, `paid_amount`, `due_amount`, `refunded_amount`, `shipping_address`, `coupon`, `custom_fields`, `note`, `follow_up_at`, `follow_up_note`, `assignee`, `delivered_at`, `created_at`, `updated_at`, `items`, `customer` (full JSON Schema in the [tool manifest](https://www.shopdeskbd.com/openapi/mcp-tools-v1.json)), and the same value as minified JSON text.

### create_customer

Creates one customer record. `phone` (Bangladeshi mobile, stored canonical) and `email` are each unique per store — a duplicate is a conflict, not a merge. When placing an order for a possibly-new buyer, prefer `create_order`’s inline `customer` (find-or-create) over calling this first. Returns the customer, addresses included.

- **Title:** Create a customer
- **Kind:** Write (adds data)
- **openWorldHint:** `false`
- **Scopes:** `customers:write`
- **Idempotency key:** honoured
- **Metering:** 1 write per call
- **REST twin:** `POST /ext/v1/customers`

| Argument | Type | Description |
| --- | --- | --- |
| `full_name` (required) | string | — |
| `phone` | string | Bangladeshi mobile, any accepted spelling. |
| `email` | string | — |
| `status` | "active" \| "inactive" \| "blocked" | Default active. |
| `gender` | "male" \| "female" | — |
| `idempotency_key` | string | A value unique to this LOGICAL operation (a UUID is ideal) — reuse the SAME value when retrying it, so a lost response can never double-run the write. Optional here, but recommended for automated callers. |
| `store_id` | uuid | Which store to write into (UUID). REQUIRED for a company-scoped key; a store-scoped key may omit it (when sent it must match the key's own store). |

Returns `structuredContent` with `id`, `store_id`, `full_name`, `email`, `phone`, `status`, `type`, `gender`, `created_at`, `updated_at`, `addresses`, `stats` (full JSON Schema in the [tool manifest](https://www.shopdeskbd.com/openapi/mcp-tools-v1.json)), and the same value as minified JSON text.

### update_customer

Patches one customer record — only the fields you send change. The same per-store uniqueness rules as `create_customer` apply to `phone` and `email`. Returns the updated customer.

- **Title:** Update a customer
- **Kind:** Write (changes existing data; safe to repeat)
- **openWorldHint:** `false`
- **Scopes:** `customers:write`
- **Idempotency key:** honoured
- **Metering:** 1 write per call
- **REST twin:** `PATCH /ext/v1/customers/{id}`

| Argument | Type | Description |
| --- | --- | --- |
| `id` (required) | uuid | The customer id. |
| `full_name` | string | — |
| `phone` | string | — |
| `email` | string | — |
| `status` | "active" \| "inactive" \| "blocked" | — |
| `gender` | "male" \| "female" | — |
| `idempotency_key` | string | A value unique to this LOGICAL operation (a UUID is ideal) — reuse the SAME value when retrying it, so a lost response can never double-run the write. Optional here, but recommended for automated callers. |
| `store_id` | uuid | Which store to write into (UUID). REQUIRED for a company-scoped key; a store-scoped key may omit it (when sent it must match the key's own store). |

Returns `structuredContent` with `id`, `store_id`, `full_name`, `email`, `phone`, `status`, `type`, `gender`, `created_at`, `updated_at`, `addresses`, `stats` (full JSON Schema in the [tool manifest](https://www.shopdeskbd.com/openapi/mcp-tools-v1.json)), and the same value as minified JSON text.

### adjust_inventory

Sets stock for one variant at one location, with a required audit `reason`. Name the variant by `variant_id` or by `sku`, and the quantity by `quantity_change` (a delta) or `set_on_hand` (the resulting total — prefer this when you know the count, and pass `expected_on_hand` to be refused rather than overwrite a number that moved). `quantity_change` is never zero, and cannot take on-hand below zero or below reservations. `type` picks the ledger category: `purchase` ADDS stock at a required `unit_cost`, `damage` must REMOVE stock, `return` can only ADD stock, default `adjustment`. A decrease filed as `adjustment` or `damage` is booked as a loss on the P&L at the current average cost. Returns the resulting stock level, the same row shape `get_inventory_levels` pages.

- **Title:** Adjust stock
- **Kind:** Write (changes existing data; safe to repeat)
- **openWorldHint:** `false`
- **Scopes:** `inventory:write`
- **Idempotency key:** required
- **Metering:** 1 write per call
- **REST twin:** `POST /ext/v1/inventory/adjustments`

| Argument | Type | Description |
| --- | --- | --- |
| `location_id` (required) | uuid | The warehouse to adjust. |
| `variant_id` | uuid | The variant being adjusted. Or resolve by `sku`. |
| `sku` | string | Resolve the variant by SKU instead of id. |
| `quantity_change` | integer | Signed units: positive adds, negative removes. Or use `set_on_hand` to state the resulting total instead. |
| `set_on_hand` | integer | The absolute on-hand this location should end at — what a stock-take says. The delta is computed under the row lock. |
| `expected_on_hand` | integer | The on-hand you believe is there; a mismatch refuses the write with CONFLICT instead of applying against a moved number. |
| `type` | "purchase" \| "return" \| "adjustment" \| "damage" | Ledger category; default `adjustment`. |
| `reason` (required) | string | Stored on the ledger entry. Required — always. |
| `unit_cost` | number | Per-unit cost. REQUIRED for `purchase` and refused on any other type; write-only, never appears in any response. |
| `low_stock_threshold` | integer | Optionally update the level’s low-stock threshold. |
| `idempotency_key` (required) | string | A value unique to this LOGICAL operation (a UUID is ideal) — reuse the SAME value when retrying it, so a lost response can never double-run the write. Required here. |
| `store_id` | uuid | Which store to write into (UUID). REQUIRED for a company-scoped key; a store-scoped key may omit it (when sent it must match the key's own store). |

Returns `structuredContent` with `id`, `store_id`, `location_id`, `variant_id`, `sku`, `on_hand`, `reserved`, `available`, `low_stock_threshold`, `is_low_stock`, `status`, `location`, `variant`, `created_at`, `updated_at` (full JSON Schema in the [tool manifest](https://www.shopdeskbd.com/openapi/mcp-tools-v1.json)), and the same value as minified JSON text.

### create_product

Creates one product with at least one variant (the sellable unit: SKU + price) — optionally with its photos and each variant’s options, in one call. `images`: up to 10 https links, which Shopdesk downloads, checks and stores before answering; one link that cannot be used fails the whole call and nothing is created. A variant’s `options` name an attribute and a value by name (any case), slug or id, e.g. `[{"attribute":"Colour","value":"Red"}]`. Nothing is created from a name: an unknown one fails the call with `candidates` (the store’s variant attributes, or that attribute’s values) — add what is missing with `create_attribute`. `image_index` points a variant at a photo by its position in `images`. `slug` auto-generates from the name and auto-suffixes when taken. `description` is stored as sanitized HTML. Stock is NOT set here — new stock enters through `adjust_inventory`, where the ledger rules live. Returns the product, the same shape `get_product` returns.

- **Title:** Create a product
- **Kind:** Write (adds data)
- **openWorldHint:** `true` — downloads the links you send
- **Scopes:** `products:write`
- **Idempotency key:** honoured
- **Metering:** 1 write per call
- **REST twin:** `GET /ext/v1/attributes`, `POST /ext/v1/products`

| Argument | Type | Description |
| --- | --- | --- |
| `name` (required) | string | — |
| `variants` (required) | object[] | At least one variant. |
| `slug` | string | — |
| `brand_id` | uuid | — |
| `short_description` | string | — |
| `description` | string | Rich description; sanitized server-side. |
| `status` | "draft" \| "active" \| "archived" | Default draft. |
| `delivery_charge` | number | Per-product override: 0 = free delivery, N = flat charge; omit for the standard shipping rules. |
| `categories` | object[] | — |
| `images` | object[] | The gallery, in display order, by https link. Add more later with `add_product_images_from_urls`. |
| `idempotency_key` | string | A value unique to this LOGICAL operation (a UUID is ideal) — reuse the SAME value when retrying it, so a lost response can never double-run the write. Optional here, but recommended for automated callers. |
| `store_id` | uuid | Which store to write into (UUID). REQUIRED for a company-scoped key; a store-scoped key may omit it (when sent it must match the key's own store). |

Returns `structuredContent` with `id`, `store_id`, `name`, `slug`, `short_description`, `description`, `thumbnail`, `status`, `delivery_charge`, `rating_average`, `rating_count`, `brand`, `categories`, `images`, `variants`, `videos`, `created_at`, `updated_at` (full JSON Schema in the [tool manifest](https://www.shopdeskbd.com/openapi/mcp-tools-v1.json)), and the same value as minified JSON text.

### update_product

Patches one product — only the fields you send change, with two REPLACE semantics to know: `categories` (when present) replaces the whole link set, and `variants` (when present) reconciles the FULL variant set — entries with an `id` are patched, entries without one are created, and existing variants missing from the list are REMOVED (refused when they carry stock history). A NEW variant takes `options` by name, like `create_product`; an existing one keeps its options. Photos are not edited here — use `add_product_images_from_urls`, and `update_variant` for a variant’s photo. `brand_id: null` detaches the brand; `delivery_charge: null` returns to the standard shipping rules. Returns the updated product.

- **Title:** Update a product
- **Kind:** Write (changes existing data)
- **openWorldHint:** `false`
- **Scopes:** `products:write`
- **Idempotency key:** honoured
- **Metering:** 1 write per call
- **REST twin:** `GET /ext/v1/attributes`, `PATCH /ext/v1/products/{id}`

| Argument | Type | Description |
| --- | --- | --- |
| `id` (required) | uuid | The product id. |
| `name` | string | — |
| `slug` | string | — |
| `brand_id` | uuid \| null | Set null to detach the brand. |
| `short_description` | string | — |
| `description` | string | — |
| `status` | "draft" \| "active" \| "archived" | — |
| `delivery_charge` | number \| null | Set null to return to the standard shipping rules. |
| `categories` | object[] | When present, REPLACES the category links. |
| `variants` | object[] | When present, reconciles the FULL variant set — see the tool description before sending a partial list. |
| `idempotency_key` | string | A value unique to this LOGICAL operation (a UUID is ideal) — reuse the SAME value when retrying it, so a lost response can never double-run the write. Optional here, but recommended for automated callers. |
| `store_id` | uuid | Which store to write into (UUID). REQUIRED for a company-scoped key; a store-scoped key may omit it (when sent it must match the key's own store). |

Returns `structuredContent` with `id`, `store_id`, `name`, `slug`, `short_description`, `description`, `thumbnail`, `status`, `delivery_charge`, `rating_average`, `rating_count`, `brand`, `categories`, `images`, `variants`, `videos`, `created_at`, `updated_at` (full JSON Schema in the [tool manifest](https://www.shopdeskbd.com/openapi/mcp-tools-v1.json)), and the same value as minified JSON text.

### archive_product

Archives one product — it leaves the sellable catalog but its history stays. There is no delete on this surface; archiving is the end state. Returns `{ id, archived }`.

- **Title:** Archive a product
- **Kind:** Write (changes existing data; safe to repeat)
- **openWorldHint:** `false`
- **Scopes:** `products:write`
- **Idempotency key:** honoured
- **Metering:** 1 write per call
- **REST twin:** `POST /ext/v1/products/{id}/archive`

| Argument | Type | Description |
| --- | --- | --- |
| `id` (required) | uuid | The product id. |
| `idempotency_key` | string | A value unique to this LOGICAL operation (a UUID is ideal) — reuse the SAME value when retrying it, so a lost response can never double-run the write. Optional here, but recommended for automated callers. |
| `store_id` | uuid | Which store to write into (UUID). REQUIRED for a company-scoped key; a store-scoped key may omit it (when sent it must match the key's own store). |

Returns `structuredContent` with `id`, `archived` (full JSON Schema in the [tool manifest](https://www.shopdeskbd.com/openapi/mcp-tools-v1.json)), and the same value as minified JSON text.

### update_order_details

Sets an order’s note and call-back (`follow_up_at`, `follow_up_note`) — nothing that changes money or stock, legal in any status. Only the fields you send change; `null` clears one. `note` REPLACES the whole note: text these tools show is shortened to 500 characters, so never rebuild a note from a value ending in "…". Returns the updated order.

- **Title:** Update an order’s note or call-back
- **Kind:** Write (changes existing data; safe to repeat)
- **openWorldHint:** `false`
- **Scopes:** `orders:write`
- **Idempotency key:** honoured
- **Metering:** 1 write per call
- **REST twin:** `PATCH /ext/v1/orders/{id}`

| Argument | Type | Description |
| --- | --- | --- |
| `order_id` (required) | uuid | The order id (UUID). Find it with `find_orders`. |
| `note` | string \| null | The order note. `null` clears it. |
| `follow_up_at` | string \| null | When to call back (ISO 8601). `null` clears it. |
| `follow_up_note` | string \| null | Why the call-back is needed. `null` clears it. |
| `idempotency_key` | string | A value unique to this LOGICAL operation (a UUID is ideal) — reuse the SAME value when retrying it, so a lost response can never double-run the write. Optional here, but recommended for automated callers. |
| `store_id` | uuid | Which store to write into (UUID). REQUIRED for a company-scoped key; a store-scoped key may omit it (when sent it must match the key's own store). |

Returns `structuredContent` with `id`, `store_id`, `number`, `source`, `channel`, `customer_id`, `location_id`, `status`, `payment_status`, `fulfillment_status`, `subtotal`, `discount_amount`, `shipping_charge`, `tax_amount`, `total_amount`, `paid_amount`, `due_amount`, `refunded_amount`, `shipping_address`, `coupon`, `custom_fields`, `note`, `follow_up_at`, `follow_up_note`, `assignee`, `delivered_at`, `created_at`, `updated_at`, `items`, `customer` (full JSON Schema in the [tool manifest](https://www.shopdeskbd.com/openapi/mcp-tools-v1.json)), and the same value as minified JSON text.

### record_call_outcome

Records the result of a confirmation call on one order, using the same status rules as the dashboard. `confirmed` moves it to confirmed; `cancelled` cancels it (stock is released). `call_back` (needs `call_back_at`) and `no_answer` (`call_back_at` optional) move it to `follow_up` — the dashboard’s call-back queue — and set the call-back time and `note` as the reason. `note` also goes on the order timeline with a status change. A status the order cannot move to is refused before anything changes. Repeating the same outcome changes nothing more. Returns the steps taken and the order.

- **Title:** Record a call outcome
- **Kind:** Write (changes existing data)
- **openWorldHint:** `false`
- **Scopes:** `orders:write`
- **Idempotency key:** honoured
- **Metering:** 1 write per call
- **response_format:** `concise` or `detailed` (default `concise`)
- **REST twin:** `GET /ext/v1/orders/{id}`, `PATCH /ext/v1/orders/{id}/status`, `POST /ext/v1/orders/{id}/cancel`, `PATCH /ext/v1/orders/{id}`

| Argument | Type | Description |
| --- | --- | --- |
| `order_id` (required) | uuid | The order id (UUID). Find it with `find_orders`. |
| `outcome` (required) | "confirmed" \| "cancelled" \| "call_back" \| "no_answer" | What the call decided. |
| `note` | string | What was said, in a sentence. |
| `call_back_at` | string | When to call again (ISO 8601). Required for `call_back`; not allowed with `confirmed` or `cancelled`. |
| `idempotency_key` | string | A value unique to this LOGICAL operation (a UUID is ideal) — reuse the SAME value when retrying it, so a lost response can never double-run the write. Optional here, but recommended for automated callers. |
| `store_id` | uuid | Which store to write into (UUID). REQUIRED for a company-scoped key; a store-scoped key may omit it (when sent it must match the key's own store). |
| `response_format` | "concise" \| "detailed" | `concise` (default): the fields needed to act, names beside ids. `detailed`: the full objects, as the REST API returns them. |

Returns `structuredContent` with `outcome`, `steps`, `order` (full JSON Schema in the [tool manifest](https://www.shopdeskbd.com/openapi/mcp-tools-v1.json)), and the same value as minified JSON text.

### create_attribute

Makes sure an attribute (Colour, Size) exists with the values you list, and returns it with all its values. If the store already has an attribute with this `name` and `type` (or this `slug`), only the values it lacks are added, compared ignoring case — nothing is renamed, changed or removed, so calling it twice adds nothing the second time; added values go after the existing ones unless `position` places them. Otherwise the attribute is created with the values in order. `type`: `variant` (default) for options that define variants (what `create_product` options name), `specification` for details that only describe a product. `input_type: "color"` lets values carry `swatch_hex`. Returns `{ attribute, created, added_values }`.

- **Title:** Create an attribute (or add its missing values)
- **Kind:** Write (adds data; safe to repeat)
- **openWorldHint:** `false`
- **Scopes:** `catalog:write` + `products:read`
- **Idempotency key:** honoured
- **Metering:** 1 write per call
- **REST twin:** `GET /ext/v1/attributes`, `POST /ext/v1/attributes`, `POST /ext/v1/attributes/{id}/values`, `GET /ext/v1/attributes/{id}`

| Argument | Type | Description |
| --- | --- | --- |
| `name` (required) | string | e.g. `Colour`. |
| `slug` | string | Look up (and create) by this slug instead of the name: lowercase letters, digits and single hyphens. |
| `type` | "variant" \| "specification" | `variant` (default) or `specification`. |
| `input_type` | "text" \| "color" \| "image" | On create only: `text` (default), `color` (values take `swatch_hex`) or `image`. |
| `is_filterable` | boolean | On create only: offer it as a storefront filter. |
| `is_required` | boolean | On create only. |
| `values` | object[] | The values it must have, in order. Each value once. |
| `idempotency_key` | string | A value unique to this LOGICAL operation (a UUID is ideal) — reuse the SAME value when retrying it, so a lost response can never double-run the write. Optional here, but recommended for automated callers. |
| `store_id` | uuid | Which store to write into (UUID). REQUIRED for a company-scoped key; a store-scoped key may omit it (when sent it must match the key's own store). |

Returns `structuredContent` with `attribute`, `created`, `added_values` (full JSON Schema in the [tool manifest](https://www.shopdeskbd.com/openapi/mcp-tools-v1.json)), and the same value as minified JSON text.

### create_category

Creates one category, top-level or under `parent` — the parent category’s name, slug or id. A parent name that matches no category, or several, fails with `candidates`; send the slug or id. Images come by https link (`image_url` for listings, `banner_url` for the category page): Shopdesk downloads and checks them before answering, and a link that cannot be used fails the call with nothing created. Each call creates a new category — check `list_categories` first. `slug` derives from the name and is suffixed when taken. Returns the category.

- **Title:** Create a category
- **Kind:** Write (adds data)
- **openWorldHint:** `true` — downloads the links you send
- **Scopes:** `catalog:write` + `products:read`
- **Idempotency key:** honoured
- **Metering:** 1 write per call
- **REST twin:** `GET /ext/v1/categories`, `POST /ext/v1/categories`

| Argument | Type | Description |
| --- | --- | --- |
| `name` (required) | string | — |
| `parent` | string | The parent category: name (any case), slug or id. Omit for a top-level category. |
| `slug` | string | — |
| `description` | string | — |
| `status` | "active" \| "inactive" | Default active. |
| `position` | integer | Sort order among siblings, lowest first. |
| `seo` | object | — |
| `image_url` | string | Listing image, by https link. Shopdesk downloads it, checks it is a PNG, JPEG, WebP or GIF (at most 10 MB) and stores its own copy. |
| `banner_url` | string | Category-page banner, by https link. Shopdesk downloads it, checks it is a PNG, JPEG, WebP or GIF (at most 10 MB) and stores its own copy. |
| `idempotency_key` | string | A value unique to this LOGICAL operation (a UUID is ideal) — reuse the SAME value when retrying it, so a lost response can never double-run the write. Optional here, but recommended for automated callers. |
| `store_id` | uuid | Which store to write into (UUID). REQUIRED for a company-scoped key; a store-scoped key may omit it (when sent it must match the key's own store). |

Returns `structuredContent` with `id`, `store_id`, `parent_id`, `name`, `slug`, `image`, `banner_image`, `description`, `seo`, `status`, `position`, `created_at`, `updated_at` (full JSON Schema in the [tool manifest](https://www.shopdeskbd.com/openapi/mcp-tools-v1.json)), and the same value as minified JSON text.

### create_brand

Creates one brand, with an optional logo (`logo_url`) and banner (`banner_url`) by https link: Shopdesk downloads and checks them before answering, and a link that cannot be used fails the call with nothing created. Each call creates a new brand — check `list_brands` first. `slug` derives from the name and is suffixed when taken. Returns the brand.

- **Title:** Create a brand
- **Kind:** Write (adds data)
- **openWorldHint:** `true` — downloads the links you send
- **Scopes:** `catalog:write`
- **Idempotency key:** honoured
- **Metering:** 1 write per call
- **REST twin:** `POST /ext/v1/brands`

| Argument | Type | Description |
| --- | --- | --- |
| `name` (required) | string | — |
| `slug` | string | — |
| `description` | string | — |
| `status` | "active" \| "inactive" | Default active. |
| `seo` | object | — |
| `logo_url` | string | Brand logo, by https link. Shopdesk downloads it, checks it is a PNG, JPEG, WebP or GIF (at most 10 MB) and stores its own copy. |
| `banner_url` | string | Brand-page banner, by https link. Shopdesk downloads it, checks it is a PNG, JPEG, WebP or GIF (at most 10 MB) and stores its own copy. |
| `idempotency_key` | string | A value unique to this LOGICAL operation (a UUID is ideal) — reuse the SAME value when retrying it, so a lost response can never double-run the write. Optional here, but recommended for automated callers. |
| `store_id` | uuid | Which store to write into (UUID). REQUIRED for a company-scoped key; a store-scoped key may omit it (when sent it must match the key's own store). |

Returns `structuredContent` with `id`, `store_id`, `name`, `slug`, `logo`, `banner_image`, `description`, `seo`, `status`, `created_at`, `updated_at` (full JSON Schema in the [tool manifest](https://www.shopdeskbd.com/openapi/mcp-tools-v1.json)), and the same value as minified JSON text.

### add_product_images_from_urls

Adds up to 10 photos to one product’s gallery from https links, in one change. Shopdesk downloads each link, checks it is a PNG, JPEG, WebP or GIF (at most 10 MB and 8000 px) and stores its own copy; the link is not kept. All or nothing: one link that cannot be used fails the call, naming `images[i].source_url`, and no photo is added. Photos are appended unless `position` says otherwise; `is_thumbnail: true` makes one the product thumbnail. A gallery holds at most 30 photos. With an `idempotency_key`, a retry answers without downloading again. Returns `{ data }`: the new photos in the order sent, with their ids.

- **Title:** Add product photos from links
- **Kind:** Write (adds data)
- **openWorldHint:** `true` — downloads the links you send
- **Scopes:** `products:write`
- **Idempotency key:** honoured
- **Metering:** 1 write per call
- **REST twin:** `POST /ext/v1/products/{id}/images`

| Argument | Type | Description |
| --- | --- | --- |
| `product_id` (required) | uuid | The product id. |
| `images` (required) | object[] | The photos, in order. |
| `idempotency_key` | string | A value unique to this LOGICAL operation (a UUID is ideal) — reuse the SAME value when retrying it, so a lost response can never double-run the write. Optional here, but recommended for automated callers. |
| `store_id` | uuid | Which store to write into (UUID). REQUIRED for a company-scoped key; a store-scoped key may omit it (when sent it must match the key's own store). |

Returns `structuredContent` with `data` (full JSON Schema in the [tool manifest](https://www.shopdeskbd.com/openapi/mcp-tools-v1.json)), and the same value as minified JSON text.

### update_variant

Edits one variant: `price`, `discount_price`, `sku`, `barcode`, `weight` and its photo (`image_id`, one of the product’s photos from `get_product`), with the dashboard’s rules — a sale price must be below the price, and the SKU is locked once the variant has stock or orders. Name the variant by id, SKU or barcode. Only the fields you send change; `null` clears `discount_price`, `barcode`, `weight` or `image_id`. Options and cost are not edited here. Returns the variant.

- **Title:** Update a variant
- **Kind:** Write (changes existing data; safe to repeat)
- **openWorldHint:** `false`
- **Scopes:** `products:write`
- **Idempotency key:** honoured
- **Metering:** 1 write per call
- **REST twin:** `PATCH /ext/v1/variants/{identifier}`

| Argument | Type | Description |
| --- | --- | --- |
| `variant` (required) | string | The variant’s id, SKU or barcode. |
| `price` | number | — |
| `discount_price` | number \| null | Sale price, below the price. `null` removes it. |
| `sku` | string | — |
| `barcode` | string \| null | `null` removes it. |
| `weight` | number \| null | Shipping weight in kg. `null` removes it. |
| `image_id` | uuid \| null | One of the product’s photo ids. `null` falls back to the product thumbnail. |
| `idempotency_key` | string | A value unique to this LOGICAL operation (a UUID is ideal) — reuse the SAME value when retrying it, so a lost response can never double-run the write. Optional here, but recommended for automated callers. |
| `store_id` | uuid | Which store to write into (UUID). REQUIRED for a company-scoped key; a store-scoped key may omit it (when sent it must match the key's own store). |

Returns `structuredContent` with `id`, `store_id`, `product_id`, `sku`, `barcode`, `name`, `price`, `discount_price`, `weight`, `dimensions`, `low_stock_threshold`, `status`, `image`, `options`, `created_at`, `updated_at` (full JSON Schema in the [tool manifest](https://www.shopdeskbd.com/openapi/mcp-tools-v1.json)), and the same value as minified JSON text.

### attach_product_video

Puts a video on a product. With `video_url` (an https link to an H.264 MP4 file — not a YouTube or other player page) Shopdesk imports the file in the background and places it at once: it shows on the product once verified, and `video.status` / `placement.status` say where it is (poll with get_product, whose `videos[]` lists ready videos). `idempotency_key` is required with `video_url`. With `video_id` (a video this key uploaded) it only places it. With neither it changes nothing and returns `upload_steps`, the calls a script makes to upload a file. `option` shows the video only for one option value of the product’s active variants, by name (e.g. `{ "attribute": "Colour", "value": "Red" }` — this reads the product, so the key needs `products:read` too; a name the product does not use answers with `candidates`), or `option_value_id` by id. If an import started but the video could not then be placed, the error names the imported video id: call again with that `video_id`. `gallery_slot` is how many photos come before it (default 1, after the main photo). Placing a video the product already shows moves it. Returns `{ video, placement, upload_steps }`.

- **Title:** Attach a video to a product
- **Kind:** Write (adds data)
- **openWorldHint:** `true` — downloads the links you send
- **Scopes:** `videos:write`
- **Idempotency key:** honoured
- **Metering:** 1 write per call
- **REST twin:** `GET /ext/v1/products/{id}`, `POST /ext/v1/videos/import`, `POST /ext/v1/products/{id}/videos`

| Argument | Type | Description |
| --- | --- | --- |
| `product_id` (required) | uuid | The product id. |
| `video_url` | string | An https link to the MP4 file itself, at most 200 MB. Shopdesk downloads it and keeps its own copy (the link is not kept). |
| `video_id` | uuid | A video this key already uploaded or imported. |
| `option` | object | Show the video only for this option value of the product’s variants, by name (needs products:read). Omit for every variant. |
| `option_value_id` | uuid | The same by id: a variant’s `options[].value_id` from get_product. |
| `title` | string | Its accessible name; defaults to the product name. |
| `gallery_slot` | integer | Photos before the video: 0 = first, default 1. |
| `sort_order` | integer | Order among the product's videos (default 0). |
| `idempotency_key` | string | A value unique to this LOGICAL operation (a UUID is ideal) — reuse the SAME value when retrying it, so a lost response can never double-run the write. Optional here, but recommended for automated callers. |
| `store_id` | uuid | Which store to write into (UUID). REQUIRED for a company-scoped key; a store-scoped key may omit it (when sent it must match the key's own store). |

Returns `structuredContent` with `video`, `placement`, `upload_steps` (full JSON Schema in the [tool manifest](https://www.shopdeskbd.com/openapi/mcp-tools-v1.json)), and the same value as minified JSON text.

## REST operations with no tool

- `GET /ext/v1/variants/{identifier}` — get_product returns every variant of the product nested, and list_products searches by variant SKU; a per-variant lookup would be a second tool answering the same question.
- `GET /ext/v1/categories/{id}` — list_categories returns the same rows; one category is never worth a tool of its own.
- `GET /ext/v1/brands/{id}` — list_brands returns the same rows; one brand is never worth a tool of its own.
- `GET /ext/v1/locations/{id}` — list_locations returns the same rows; a store has a handful of locations.
- `GET /ext/v1/inventory/movements` — The stock ledger is sync data for ERPs and accounting, read over REST; get_inventory_levels answers the conversational question (how much is there).
- `POST /ext/v1/products/{id}/images/reorder` — Reordering photos is install-time catalog work (§5.1: photo delete, reorder and alt-text edits stay REST-only), and it restates the whole gallery, which a model gets wrong too easily.
- `PATCH /ext/v1/products/{id}/images/{imageId}` — Photo alt-text, order and main-photo edits are install-time catalog work (§5.1); add_product_images_from_urls sets alt text, position and the main photo when it adds a photo.
- `DELETE /ext/v1/products/{id}/images/{imageId}` — A permanent photo delete is kept to REST (§5.1) so a conversation cannot remove catalog media.
- `PATCH /ext/v1/attributes/{id}` — Renaming an attribute or changing its type or flags is install-time catalog work; create_attribute never renames or removes anything.
- `PATCH /ext/v1/attributes/{id}/values/{valueId}` — Renaming a value relabels every variant that shows it — catalog correction work, kept to REST and the dashboard.
- `PATCH /ext/v1/categories/{id}` — Category edits (moves, renames, image swaps) are install-time catalog work; create_category covers the conversational case.
- `PATCH /ext/v1/brands/{id}` — Brand edits (renames, logo swaps) are install-time catalog work; create_brand covers the conversational case.
- `POST /ext/v1/videos` — Starting an upload session is binary work for a script or the SDK: the caller then PUTs file parts to storage, which a model cannot do (§5.1). attach_product_video takes a finished upload’s video_id.
- `POST /ext/v1/videos/{id}/part-urls` — Part URLs exist only to PUT file bytes to storage — binary is not tool-shaped (§5.1).
- `POST /ext/v1/videos/{id}/complete` — Completing an upload follows the byte transfer a script makes; it is part of the same upload session (§5.1).
- `GET /ext/v1/videos` — Upload-session status for the script doing the upload (§5.1); get_product shows a product’s ready videos.
- `GET /ext/v1/videos/{id}` — Upload-session status for the script doing the upload (§5.1); get_product shows a product’s ready videos.
- `DELETE /ext/v1/videos/{id}` — Video deletes are kept to REST (§5.1: video posters and deletes), so a conversation cannot remove catalog media.
- `POST /ext/v1/videos/{id}/poster` — Video covers are kept to REST (§5.1: video posters and deletes): an uploaded cover is binary, and a product page falls back to the product thumbnail without one.
- `DELETE /ext/v1/products/{id}/videos/{videoId}` — Taking a video off a product is a media delete, kept to REST (§5.1: video posters and deletes) so a conversation cannot remove catalog media; attach_product_video only adds or moves a video.
- `GET /ext/v1/reports/top-products` — EXPANSION_PLAN §5.1 lists no tool for the ranking: get_sales_summary answers the conversational sales question, and the tool budget is held for the order desk. A BI sync reads the ranking over REST; adding a tool later means moving this line to MCP_REST_TWIN.
