MCP Server
STOQ ships a built-in MCP server over the v2 external API. Rather than exposing one MCP tool per action — hundreds of tool schemas loaded into every client's context before a single call is made — the full catalog (preorders and back-in-stock, 200+ actions) is reached through three tools: search, execute_read, execute_write. All three read the same registry that drives HTTP dispatch and the /help manifest, so a new action is searchable and callable the moment it ships — there is nothing to update on your side.
This means an AI assistant connected to the server can do anything the API can do: list offers, change widget text, schedule campaigns, release preorder fulfillments, pull back-in-stock signup reports, and so on — scoped to your shop, under your API key.
Endpoint & auth
POST https://app.stoqapp.com/api/v2/external/mcpThe 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 APIAuthorization: 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.
Connecting a client
Claude Code
claude mcp add --transport http stoq https://app.stoqapp.com/api/v2/external/mcp \
--header "X-Auth-Token: <your-api-key>"Claude Desktop
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"
}
}
}
}Cursor
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.
The three tools
search
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.
execute_read / execute_write
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.
Aliases drive intent matching
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.
PATCH actions carry the toggles
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, ...).
Result shape
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). statusmirrors 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.
Rate limits
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.
A worked example
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": [
// { "id": "9c2f6a1e-…", "name": "Summer Drop", "status": "enabled", … },
// …
// ] } }
// 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.
More examples
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.
Preorder offers
"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 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 off the default "whole product" source, then add the variants
{ "method": "POST", "path": "/preorders/offers/a1b2c3d4-…/products/set_source_to_custom", "params": {} }
{ "method": "POST", "path": "/preorders/offers/a1b2c3d4-…/products/add_variants",
"params": { "variant_ids": ["45000000001", "45000000002", "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": "POST", "path": "/preorders/offers/a1b2c3d4-…/markets/set_markets",
"params": { "market_ids": ["gid://shopify/Market/1", "gid://shopify/Market/2"] } }Preorder orders
"Which orders from the fall boots preorder are ready to fulfill?"
// tool: execute_read
{ "method": "GET", "path": "/preorders/orders",
"params": { "offer_id": "a1b2c3d4-…", "state": "ready_to_release" } }GET /preorders/orders also filters by variant_id, 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": ["ord_1", "ord_2", "ord_3"] } }
// → bulk actions return { "job_id": "…" } — poll the jobs action for this offer to see per-order results"Cancel this order and refund the deposit — the customer asked."
// tool: execute_write
{ "method": "POST", "path": "/preorders/orders/ord_1/cancel",
"params": { "reason": "customer_request", "refund_deposit": true, "notify_customer": true } }reason is one of customer_request, merchant_decision, inventory_unavailable, other — it lands on the cancellation report, so pick the real one rather than defaulting to other.
"Charge the remaining balance now that it's back in stock."
// tool: execute_write
{ "method": "POST", "path": "/preorders/orders/ord_1/payments/charge_balance",
"params": { "mode": "auto" } }Reports
"How much preorder revenue did we do last month, broken out by week?"
// tool: execute_read
{ "method": "GET", "path": "/preorders/reports/revenue",
"params": { "from": "2026-07-01", "to": "2026-07-31", "granularity": "week" } }Also filters by offer_id / offer_ids, product_id, variant_id, and market_id, and can split include_deposits from include_balances — handy for "how much have we actually collected vs. still owed."
Back in stock
"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/push), 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/bulk_notify",
"params": { "signup_ids": ["sgn_a1b2c3", "sgn_d4e5f6"] } }Or notify one signup at a time with POST /back_in_stock/signups/:id/notify — both take 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.
Building an agent that can't speak MCP? The same discovery surface is available as plain HTTP: see Integrating AI agents for the skill.md + /help manifest workflow.
