MCP Server
STOQ ships a built-in MCP server over the v2 external API. Every registered v2 action — preorders and back-in-stock, roughly 187 in total — is exposed as one MCP tool, generated from the same class-level declarations that drive HTTP dispatch and the /help manifest. When STOQ ships a new action, it becomes an MCP tool automatically; 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.
Tool naming
Tool names are the action's URL path with placeholders dropped, joined by underscores, suffixed with the action name when the path doesn't already end in it:
| HTTP action | MCP tool |
|---|---|
GET /preorders/offers | preorders_offers_list |
PATCH /preorders/offers/:id | preorders_offers_update |
POST /preorders/offers/:id/widget/set_button_text | preorders_offers_widget_set_button_text |
GET /back_in_stock/notifications | back_in_stock_notifications_list |
Path placeholders (:id etc.) become required string properties on the tool's input schema — there is no URL in an MCP call. So preorders_offers_widget_set_button_text takes { "id": "<offer-id>", "text": "..." }.
Aliases drive intent matching
Each tool's description embeds the action's natural-language aliases — the phrasings merchants actually use. preorders_offers_widget_set_button_text 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 tool out of ~187 without any custom prompting.
PATCH tools carry the toggles
The same convention as the HTTP API applies: named tools carry real side effects; plain settings — including every boolean toggle — live on the capability's PATCH tool. There is no enable_badge tool; flipping the badge is preorders_offers_widget_update with { "id": "...", "badge": { "enabled": true } }. PATCH tools are deep-partial (fields you don't send are left alone), and each PATCH tool's description notes 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 two tool calls:
// 1. The name "summer drop" isn't an ID — list offers to find it.
// tool: preorders_offers_list
{ }
// → { "success": true, "data": { "offers": [
// { "id": "9c2f6a1e-…", "name": "Summer Drop", "status": "enabled", … },
// …
// ] } }
// 2. Aliases match "change button text" → set_button_text.
// tool: preorders_offers_widget_set_button_text
{ "id": "9c2f6a1e-…", "text": "Reserve yours" }
// → { "success": true, "data": { …updated offer… } }The same pattern — resolve a name to an ID with a list tool, then call the intent-bearing tool — covers most merchant requests.
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.
