Skip to content

MCP Server

Connect Claude, Cursor, or any MCP client to STOQ — learn what it can do and read the docs with no sign-in, then reach the whole 200+ action API through a handful of tools instead of hundreds.

BetaAPI v2 is in public beta: endpoints, fields and behaviour can change before general availability. See the changelog

Last updated

STOQ ships a built-in MCP server over the v2 external API. It exposes a small, fixed tool set in two groups:

  • Docs tools — learn_stoq, find_doc, read_doc: public, no sign-in, no store data. Start here — learn_stoq is a primer on what STOQ is and how the tools fit together; find_doc/read_doc search and read the help centre and developer docs.
  • Store tools — search, execute_read, execute_write, generate_storefront_widget, search_help_docs: scoped to one shop under your sign-in or API key. Instead of one MCP tool per action (hundreds of schemas loaded before a single call), the whole catalog — preorders, back-in-stock and account, 200+ actions — is reached through search and the two execute tools, read from the same registry that drives HTTP dispatch and the /help manifest, so a new action is searchable and callable the moment it ships.
Tool Title Group Reads or writes
search Search STOQ API Actions Store Reads
execute_read Execute STOQ Read Store Reads
execute_write Execute STOQ Write Store Writes
generate_storefront_widget Generate Storefront Widget Store Reads
search_help_docs Search STOQ Help Docs Store Reads
learn_stoq Learn STOQ Docs Reads
find_doc Find STOQ Docs Docs Reads
read_doc Read STOQ Doc Docs Reads

The machine-readable list, with each tool’s input schema and annotations, is at https://app.stoqapp.com/api/v2/mcp.json.

The flow: learn_stoq to orient and find_doc to find the right action or guide → connect a store → search + execute_read/execute_write to do anything the API can do: list offers, change widget text, schedule campaigns, release preorder fulfillments, pull back-in-stock signup reports.

POST https://app.stoqapp.com/api/v2/external/mcp

Docs only, no sign-in (learn_stoq, find_doc, read_doc; never store data):

POST https://app.stoqapp.com/api/v2/external/mcp/docs

The connector URL above always requires sign-in; a signed-in session also gets these docs tools.

The server speaks MCP over HTTP (JSON-RPC in the request body). Auth uses the same per-shop API key as the rest of the v2 surface, sent either way:

  • X-Auth-Token: <key> — same header as the REST API
  • Authorization: Bearer <key> — for MCP clients that only support Authorization headers

You can find your API key in the STOQ app: Settings → Integrations → API Key. See API Key for the walkthrough.

The key grants full read/write access to your shop’s preorder and back-in-stock data. Treat it like a password — anyone (or any agent) holding it can modify live offers.

Terminal window
claude mcp add --transport http stoq https://app.stoqapp.com/api/v2/external/mcp \
--header "X-Auth-Token: <your-api-key>"

Claude Desktop launches local processes, so use the mcp-remote bridge in claude_desktop_config.json:

{
"mcpServers": {
"stoq": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://app.stoqapp.com/api/v2/external/mcp",
"--header",
"X-Auth-Token:${STOQ_API_KEY}"
],
"env": {
"STOQ_API_KEY": "your-api-key"
}
}
}
}

In .cursor/mcp.json (project) or ~/.cursor/mcp.json (global):

{
"mcpServers": {
"stoq": {
"url": "https://app.stoqapp.com/api/v2/external/mcp",
"headers": {
"X-Auth-Token": "your-api-key"
}
}
}
}

Any other MCP client that supports HTTP transport with custom headers works the same way — point it at the endpoint and send the key.

Reads Preorder offers, their variants and settings; preorder orders, payments and fulfilment timelines; back-in-stock signups, their notification history and settings; preorder and back-in-stock reports; account settings, integrations, markets and languages; the help centre and these developer docs.
Writes Create, duplicate, schedule, enable, disable and edit preorder offers (widget, payments, shipping, limits, markets, translations, variants); release, hold and reschedule preorder orders, and send their customer updates; add, notify, transfer and delete back-in-stock signups; change back-in-stock and account settings.
Without sign-in learn_stoq, find_doc and read_doc. They read docs only, never store data.
Sign-in OAuth 2.1 with PKCE. A client that gets a 401 from the endpoint finds the server through /.well-known/oauth-protected-resource and /.well-known/oauth-authorization-server, and registers itself (Dynamic Client Registration, RFC 7591). The merchant then approves the connection in STOQ. The token is for that one store. Claude and ChatGPT connectors work this way; see Connect AI assistants. The API key above is for scripts and for clients without OAuth.
Payments Actions that charge a customer’s saved card (collect_payment, bulk_collect_payment) are writes like any other. Confirm with the merchant before you call one.

Never ask a merchant to paste an API key into a chat. Connect over OAuth instead.

Keyword search over every action’s path, description, aliases, and notes — e.g. "deposit", "widget button color", "cancel order", "back in stock signup". Returns method + path + description for each match:

// tools/call search { "query": "rename the preorder button" }
{
"count": 2,
"returned": 2,
"actions": [
{ "method": "POST", "path": "/preorders/offers/:id/widget/set_button_text",
"description": "Set the preorder button's call-to-action label.",
"aliases": ["change button text", "rename button", "set button label", "..."] },
{ "method": "POST", "path": "/preorders/offers/:id/advanced/set_button_text_override", "..." : "..." }
]
}

A search that narrows to 3 or fewer matches also includes the full request JSON Schema for each action (request_schema, path_params) — everything execute_read/execute_write need. Broader searches stay compact so browsing doesn’t itself flood context. Call search with an empty query to browse the whole catalog.

Call the concrete method + path a search result gave you, with real ids substituted for any :placeholder segment, plus params for the request body:

// tools/call execute_write
{
"method": "POST",
"path": "/preorders/offers/9c2f6a1e-.../widget/set_button_text",
"params": { "text": "Reserve yours" }
}

execute_read only accepts GET; execute_write accepts POST, PATCH, DELETE. An unregistered path — or one registered under a different method — returns a structured error ({ success: false, status: "not_found", errors: [...] }) rather than raising, and hints at the correct method when it can.

execute_write also takes an optional idempotency_key. Send a fresh one (a new UUID, say) with every write, and the same one only to retry that write. Keys are remembered for 1 hour: the same key with the same arguments within that hour returns the first result (or, for a large result or one with customer data, idempotent_replay_ref naming what was written), marked idempotent_replay: true, without writing again. See Idempotency.

response_format: detailed by default, concise on request

Section titled “response_format: detailed by default, concise on request”

execute_read takes response_format: "detailed" (the default) or "concise". Ask for concise when you want an overview or are reading many rows. It keeps an agent’s context small:

  • List actions return 20 rows a page unless you send per_page.
  • Fields that are null or an empty string are left out. Empty lists stay, since they say “none”.
  • In every object, readable fields (name, title, internal_name, status, …) come first, then the ids, which are always kept for follow-up calls.
  • A list inside a row (an offer’s variants, say) shows its first 25 items.
  • The JSON is compact, not pretty-printed.

"detailed" returns data exactly as the REST API does. The REST API itself is unchanged: response_format exists only on the MCP tool.

Both formats are capped: about 6,000 tokens (24,000 bytes) for concise (12,000 tokens, 48,000 bytes, when you send your own per_page of 50 or less) and 25,000 tokens (100,000 bytes) for detailed. A response is never cut silently. When a page of rows has more pages, or was cut to fit the cap, next_page is the exact call for the rows that follow, and anything cut is listed under truncated:

{
"success": true,
"data": { "offers": [ { "name": "Summer Drop", "current_status": "live", "id": 812, "...": "..." } ],
"meta": { "page": 1, "per_page": 20, "total_count": 64, "total_pages": 4 } },
"next_page": { "tool": "execute_read",
"arguments": { "method": "GET", "path": "/preorders/offers", "params": { "per_page": 20, "page": 2 },
"response_format": "concise" } }
}
// cut to fit the cap
"truncated": {
"note": "This response was cut to fit the 6000-token cap (24000 bytes). Narrow the read with filters, a smaller per_page or one id. next_page continues exactly where these rows stop. response_format: \"detailed\" allows up to 25000 tokens.",
"lists": [ { "path": "data.offers", "shown": 10, "total": 20, "next_page_continues": true } ]
}

Call next_page.arguments as given. It never skips a row:

  • If the page of rows was cut, it sends a smaller per_page. A cursor-paged action reads the same page again at the smaller size instead, and its cursor then starts after the rows you have.
  • A cut list that isn’t the page of rows, such as a list beside it or a list inside a row, has "next_page_continues": false. Read those rows by id or with filters.
  • A row too big to send alone keeps its ids and readable fields, and its largest fields are left out. They are listed under truncated.omitted (for example "data.offers[0].template"). Read that row by id with response_format: "detailed".

Docs tools: learn_stoq, find_doc, read_doc

Section titled “Docs tools: learn_stoq, find_doc, read_doc”

The docs tools are public — no sign-in, no store data; they return the same content for every caller.

  • learn_stoq(topic?): a primer to call first. It covers what STOQ is, how to connect a store, how the tools fit together, the conventions agents trip on, and the top tasks with their doc ids and actions. Topics: overview (default), preorders, back_in_stock, storefront, data_contract, auth.
  • find_doc(query, audience, limit?): searches the help centre (merchant), these developer guides with one reference page per API action and the metafield data contract (developer), or all of them. audience is required: pick it by what the question is about. merchant for using the app, developer for the API, webhooks, SDK, metafields or code, all when the question spans both. Each hit has a doc_id, title, snippet, URL, source (help, developer, api or contract) and the API actions the page documents.
  • read_doc(doc_ids, offset?): the full Markdown of up to 5 docs, by doc_id (for example help:/general/ai-connectors/) or by their help.stoqapp.com / docs.stoqapp.com URL. A long page comes back in 6,000-character parts; pass next_offset as offset to read on.

Each action’s search result carries its natural-language aliases — the phrasings merchants actually use. The button-text action carries “change button text”, “rename button”, “set button label”, and so on. This is what lets a model resolve “rename the preorder button” to the right action out of 200+ from a single search call.

The same convention as the HTTP API applies: named actions carry real side effects; plain settings — including every boolean toggle — live on the capability’s PATCH action. There is no “enable badge” action; flipping the badge is PATCH /preorders/offers/:id/widget with { "badge": { "enabled": true } }. PATCH actions are deep-partial (fields you don’t send are left alone), and each one’s notes field (visible in a narrow search result) lists its toggleable field paths (badge.enabled, disclaimer.enabled, button.colors.enabled, …).

Every tool returns a JSON text payload with an explicit success flag:

// success
{ "success": true, "data": { "id": "uuid", "name": "Summer Drop", "...": "..." } }
// failure
{ "success": false, "status": "conflict", "errors": ["Offer is discarded; call restore first."] }
  • Reads return the resource in data; writes return the updated resource, or { "job_id": ... } for bulk/async work — poll the matching jobs tool (see Bulk & Async Jobs).
  • status mirrors the HTTP error classes: unauthorized, not_found, unprocessable_entity, conflict.

status: "conflict" means an invalid lifecycle transition (e.g. enabling an offer that’s discarded). The error message names the action that unblocks the transition — read it and adjust the plan. Don’t retry the same call blindly.

A store tool (search, execute_read, execute_write, generate_storefront_widget, search_help_docs) called without a usable credential doesn’t run. It returns a tool result with isError: true: a short Markdown explanation, and a setup object as structuredContent that says how to connect and what to do next.

{
"error": "auth_required",
"code": "auth_required",
"tool": "execute_write",
"message": "This tool reads or changes one Shopify store, so STOQ needs that store's permission first. Nothing was run.",
"auth": { "kind": "per_shop", "methods": [{ "id": "oauth_connector", "...": "..." }, { "id": "api_key", "header": "X-Auth-Token", "...": "..." }], "scopes": ["shop"] },
"steps": [{ "actor": "merchant", "text": "..." }, { "actor": "agent", "text": "..." }],
"docs": { "find_doc": { "query": "connect an AI assistant to STOQ", "source": "help" }, "read_doc": "help:/general/ai-connectors/", "learn_stoq": "auth" },
"retry": { "after": "merchant_action", "same_call": true },
"public_tools": ["learn_stoq", "find_doc", "read_doc"]
}
code When retry.after
auth_required No credential was sent merchant_action
auth_invalid A credential was sent but isn’t valid, or its store no longer has STOQ installed. This one is an HTTP 401 with WWW-Authenticate (so connectors refresh or sign in again), with the same object as the body reconnect
shopify_reauth_required Reserved: STOQ’s Shopify access to the store has expired; the merchant opens STOQ in their Shopify admin once token_refresh

Follow steps, then retry the same call. The object never contains a store domain, a key, or whether any store has STOQ installed.

MCP tool calls hit the same cost-weighted rate limiter as REST calls — reads cost 1 point, writes cost 2, against the same per-token budget. See Rate Limits. Agents running multi-step workflows should expect occasional 429-equivalent failures and back off.

Merchant asks their assistant: “Change the preorder button text on the summer drop to ‘Reserve yours’.”

A connected client resolves this in three tool calls:

// 1. Find the list-offers action.
// tool: search { "query": "list offers" }
// → { method: "GET", path: "/preorders/offers", ... }
// 2. The name "summer drop" isn't an ID — call it to find the offer.
// tool: execute_read { "method": "GET", "path": "/preorders/offers", "params": {} }
// → { "success": true, "data": { "offers": [
// { "name": "Summer Drop", "current_status": "live", "id": "9c2f6a1e-…", … },
// …
// ] } }
// 3. Aliases match "change button text" → set_button_text. search narrows to it,
// giving the exact path + request schema, then call it with the real offer id.
// tool: execute_write
{ "method": "POST", "path": "/preorders/offers/9c2f6a1e-…/widget/set_button_text",
"params": { "text": "Reserve yours" } }
// → { "success": true, "data": { …updated offer… } }

The same pattern — search for the action, resolve a name to an ID with a read, then execute the intent-bearing action — covers most merchant requests. The examples below skip straight to the execute_read/execute_write call for brevity; in a real session, search is what found that method + path in the first place.

A spread of the things merchants actually ask an assistant to do, grouped by area. Every example assumes you already have the offer/order/signup id — from a search → list/read round trip, same as above.

“Set up a new preorder for the restock.”

// tool: execute_write
{ "method": "POST", "path": "/preorders/offers",
"params": { "name": "Preorder", "internal_name": "Fall Boots Restock" } }
// → { "success": true, "data": { "id": "a1b2c3d4-…", "status": { "state": "draft", … }, … } }

“Turn on the fall boots preorder.” / “Pause it.”

// tool: execute_write
{ "method": "POST", "path": "/preorders/offers/a1b2c3d4-…/enable", "params": {} }
{ "method": "POST", "path": "/preorders/offers/a1b2c3d4-…/disable", "params": {} }

Both accept optional update_inventory_policy and shopify_variant_ids if you want the toggle to also flip Shopify’s continue-selling policy for those variants — omit them to just change offer state.

“Launch it next Monday and close signups two weeks later.”

// tool: execute_write
{ "method": "POST", "path": "/preorders/offers/a1b2c3d4-…/schedule",
"params": { "start": "2026-08-31T00:00:00Z", "end": "2026-09-14T00:00:00Z" } }

“Require a 25% deposit instead of full payment up front.”

// tool: execute_write
{ "method": "POST", "path": "/preorders/offers/a1b2c3d4-…/payments/set_deposit_percent",
"params": { "percent": 25 } }

“Only these three variants should be on preorder, not the whole product.”

// tool: execute_write — switch the offer to specific products, then add the variants
{ "method": "PATCH", "path": "/preorders/offers/a1b2c3d4-…/products", "params": { "selection": "custom" } }
{ "method": "POST", "path": "/preorders/offers/a1b2c3d4-…/products/add_variants",
"params": { "shopify_variant_ids": ["gid://shopify/ProductVariant/45000000001", "gid://shopify/ProductVariant/45000000002", "gid://shopify/ProductVariant/45000000003"] } }

“Limit customers to 3 per order.”

// tool: execute_write
{ "method": "POST", "path": "/preorders/offers/a1b2c3d4-…/limits/set_max_per_order",
"params": { "max": 3 } }

“Tell customers it ships September 15th.”

// tool: execute_write
{ "method": "POST", "path": "/preorders/offers/a1b2c3d4-…/shipping/set_delivery_date",
"params": { "date": "2026-09-15" } }

“Only sell this preorder in the US and Canada.”

// tool: execute_write
{ "method": "PATCH", "path": "/preorders/offers/a1b2c3d4-…/markets",
"params": { "scope": "selected", "shopify_market_ids": ["gid://shopify/Market/1", "gid://shopify/Market/2"] } }

“Which orders from the fall boots preorder are ready to fulfill?”

// tool: execute_read
{ "method": "GET", "path": "/preorders/orders",
"params": { "offer_id": "a1b2c3d4-…", "preorder_fulfillment_status": "on_hold" } }

GET /preorders/orders also filters by shopify_variant_id, shopify_product_id, vendors, customer_id, and a from/to date range — useful for “orders placed last week” style requests.

“Release everything that’s ready.”

// tool: execute_write
{ "method": "POST", "path": "/preorders/orders/bulk_release",
"params": { "order_ids": ["d608978f-…", "gid://shopify/Order/6123456789012"] } }
// → bulk actions return { "job_id": "…", "status_url": "…" }; poll GET /preorders/orders/jobs/:job_id for per-order results

“Charge the remaining balance now that it’s back in stock.”

// tool: execute_write
{ "method": "POST", "path": "/preorders/orders/gid://shopify/Order/6123456789012/collect_payment",
"params": {} }

“How much preorder revenue did we do last month, day by day?”

// tool: execute_read
{ "method": "GET", "path": "/preorders/reports/by_date",
"params": { "from": "2026-07-01", "to": "2026-07-31" } }

The same points as the Preorders Overview charts. The bucket size is the chart’s: daily up to 60 days, weekly up to 180, then monthly. For per-product numbers read /preorders/reports/products.

“Who’s waiting on the sold-out blue medium?”

// tool: execute_read
{ "method": "GET", "path": "/back_in_stock/signups",
"params": { "variant_id": "45000000004", "status": "pending" } }

Also filters by product_id, email, phone, channel (email/sms), and a from/to signup-date range.

“It’s back in stock — notify everyone waiting.”

// tool: execute_write
{ "method": "POST", "path": "/back_in_stock/signups/notify_variants",
"params": { "shopify_variant_ids": ["gid://shopify/ProductVariant/12345"] } }

Use POST /back_in_stock/signups/notify to send to everyone matching waitlist filters (status, channel, limit and so on), or POST /back_in_stock/signups/:id/notify for one signup, which takes an optional allow_resend if the customer was already notified once (e.g. a second restock).

“Batch restock alerts in groups of 50 instead of emailing everyone at once.”

// tool: execute_write — PATCH is deep-partial; only the fields you send change
{ "method": "PATCH", "path": "/back_in_stock/settings/delivery",
"params": { "batching": { "enabled": true, "mode": "fixed", "batch_size": 50 } } }

The same action’s stock_threshold (minimum quantity before alerts start going out) and any_variant/locations scoping live alongside batching — send only what you’re changing.

Was this page helpful?

Still stuck? Ask Quinn or chat with support