Home
AI & MCP

MCP Server

Connect Claude, Cursor, or any MCP client to the v2 API — search the action catalog and call anything the API can do, through 3 tools instead of hundreds.

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/mcp

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.

Warning

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

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).
  • status mirrors the HTTP error classes: unauthorized, not_found, unprocessable_entity, conflict.
Warning

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.

Note

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.