MCP tools
The full tool surface available via /api/mcp and /api/v1/tools.
Cartwright exposes 89 tools (the live catalog at GET /api/v1/tools on any shop is always the authority — this page is a guide, not a registry). Every tool can be called by an external AI agent (MCP at /api/mcp), a REST client (/api/v1/tools), the storefront chat assistant, or the admin AI — all funneled through the same lib/tools/registry.ts.
This page is a reference catalogue. For the validation pipeline and audit-log behaviour see Architecture: tool registry.
Authentication
All MCP and REST calls require a bearer token from an API key created in /admin/api-keys. Each key carries an explicit list of scopes; tools verify scope before executing.
curl https://my-shop.com/api/mcp \
-H "Authorization: Bearer sb_live_..." \
-H "Content-Type: application/json" \
-d '{"method":"tools/list"}'For the strict input/output JSON Schema for each tool, hit GET /api/v1/tools (it serializes Zod schemas via zodToJsonSchema).
Scopes catalogue
Scopes are defined in lib/scopes.ts. 21 scopes total across the read/write split:
products:read, products:write
categories:read, categories:write
pages:read, pages:write
discounts:read, discounts:write
orders:read, orders:write
settings:read, settings:write
audit:read, audit:revert
analytics:read
marketing:write
catalog:read ← read-only catalog access for storefront chat
cart:write ← cart mutations for storefront chat
customer:read ← customer lookup for cart/account context* exists as a wildcard for admin UI keys. Never grant it to a storefront chat session.
Several product/category read tools require catalog:read rather than products:read or categories:read. The latter scopes exist for symmetry but are not currently required by any registered tool. When granting a key, prefer catalog:read for storefront/agent reads.
Tool catalogue
| Tool | Scope | Audit |
|---|---|---|
products.search | catalog:read | skip |
products.get | catalog:read | skip |
products.create | products:write | yes |
products.update | products:write | yes |
products.delete | products:write | yes |
products.attach_image | products:write | yes |
| Tool | Scope | Audit |
|---|---|---|
categories.list | catalog:read | skip |
categories.upsert | categories:write | yes |
categories.delete | categories:write | yes |
| Tool | Scope | Audit |
|---|---|---|
orders.list | orders:read | skip |
orders.get | orders:read | skip |
orders.create | orders:write | yes |
orders.update_status | orders:write | yes |
orders.update_status moves no money. It refuses refunded and partial_refund with
refund_requires_payment_path, because a card refund made on the admin order page or in Stripe sets
that status itself. To record a refund made outside the shop, such as a bank transfer, send
manual: true and confirm: true. A partial_refund also needs refundDkk: the amount in øre, as a positive
whole number below the order total.
The order then gets a note saying no money moved, and the result carries moneyMoved: false.
Cancelling with orders.update_status (status: "cancelled") does two more things. An order still
pending_payment gets its items back in stock, exactly once, and the result carries restocked: true;
if the order stopped awaiting payment between the read and the write, the call fails with "no longer
awaiting payment" and leaves the order unchanged. An order with a Stripe payment also has its PaymentIntent
cancelled if it is still unpaid, and the result's paymentIntent says what happened: outcome is
canceled, already_canceled, succeeded or failed, with the timeline note. A succeeded payment
is left alone and is refunded on the admin order page. Engines up to v0.60.0 return no stock here and
carry no restocked; engines up to v0.59.1 leave the payment open and carry no paymentIntent. The
whole rule: Checkout (Stripe).
| Tool | Scope | Audit |
|---|---|---|
discounts.list | discounts:read | skip |
discounts.create | discounts:write | yes |
discounts.toggle | discounts:write | yes |
discounts.try_apply | cart:write | yes |
| Tool | Scope | Audit |
|---|---|---|
pages.list | pages:read | skip |
pages.upsert | pages:write | yes |
pages.delete | pages:write | yes |
| Tool | Scope | Audit |
|---|---|---|
settings.get | settings:read | skip |
settings.update_branding | settings:write | yes |
settings.update_shipping | settings:write | yes |
| Tool | Scope | Audit |
|---|---|---|
analytics.summary | analytics:read | skip |
| Tool | Scope | Audit |
|---|---|---|
marketing.create_campaign | marketing:write | yes |
| Tool | Scope | Audit |
|---|---|---|
audit.list | audit:read | skip |
audit.revert | audit:revert | yes |
| Tool | Scope | Audit |
|---|---|---|
images.search_unsplash | products:write | skip |
Requires UNSPLASH_ACCESS_KEY env. Falls back gracefully if absent.
| Tool | Scope | Audit |
|---|---|---|
address.autocomplete | catalog:read | skip |
| Tool | Scope | Audit |
|---|---|---|
cart.add | cart:write | skip |
cart.update_quantity | cart:write | skip |
cart.remove | cart:write | skip |
cart.get_summary | cart:write | skip |
customer.lookup_by_email | customer:read | skip |
customer.lookup_by_phone | customer:read | skip |
user.get_last_shipping | customer:read | skip |
Cart and customer tools assume a storefront chat session with a cookie-based customer context. An API-key-only call with cart:write scope will accept the call but operate on an empty session — you usually want these tools called by the chat assistant, not directly by an external agent.
Example: invoke from an external agent
curl https://my-shop.com/api/mcp \
-H "Authorization: Bearer sb_live_..." \
-H "Content-Type: application/json" \
-d '{
"method": "tools/call",
"params": {
"name": "products.search",
"arguments": { "q": "aviator", "limit": 5 }
}
}'For the exact response envelope and JSON Schema of each tool's inputSchema, query GET /api/v1/tools against your shop.