Skip to main content
Glama
Cubitrek

shopify-multistore-mcp

by Cubitrek

shopify-multistore-mcp

An MCP server for working across multiple Shopify stores (multi-client dev work) through the Admin GraphQL API. Every tool takes a store argument that routes the call to the right client store, so one connector covers all your stores.

Designed to sit alongside Shopify's official tooling, not replace it:

Layer

What it does

You build it?

Shopify AI Toolkit / Dev MCP

Theme, apps, Functions, extensions, docs + GraphQL validation

No — install it

Shopify CLI

theme dev, app dev, Functions deploy (per-store auth)

No — install it

This server

Conversational Admin API read/write across stores, with a production write-gate

Yes — this repo

Quick start

git clone https://github.com/Cubitrek/shopify-multistore-mcp.git
cd shopify-multistore-mcp
npm install
npm run build
cp stores.example.json stores.json    # then edit — add your stores

Then, per store, get an access token and connect it to Claude. Check your config loaded:

STORES_CONFIG=./stores.json node -e \
  'import("./dist/config.js").then(m=>console.table(m.loadRegistry().list()))'

Every store shows authenticated: true or false, so you can see at a glance which ones still need a token.

Related MCP server: Shopify Store MCP Server

Tools

Reads: list_stores, shop_info, get_products, get_orders, get_metafields, get_pages

Writes (production-gated): set_metafields, add_tags, remove_tags, create_product, update_product, create_page, update_page

Escape hatch: graphql — run any Admin GraphQL query or mutation

The production write-gate

Each store is tagged env: "production" or env: "dev".

  • Mutations against a dev store run freely.

  • Mutations against a production store are blocked unless the call passes confirm: true. The model must inspect what will change, then explicitly confirm.

Failed writes surface their userErrors, so a rejected mutation can never look like a success.

Getting an access token

Which path you need depends on the store's relationship to your Partner organization — not on how old the store is. Check this first; picking wrong costs an hour of dead ends:

The store is…

Where you see it

Path

A merchant's own store you have collaborator access to, or your own live store

Dev Dashboard → Stores → Collaborations

Path A — OAuth capture

A client-transfer store still held in your Partner org

Dev Dashboard → Stores → Client transfer

Path B — legacy custom app

Already has a legacy custom app from before 2026

Store admin → Settings → Apps

Reuse it — copy the existing shpat_…

⚠️ Custom apps cannot be installed on client-transfer stores. Shopify restricts those to free and partner-friendly apps, so every install attempt fails with "The installation link for this app is invalid" — no matter how the app is configured. This is not a misconfiguration; Path A simply cannot work until the store is transferred to the merchant. Use Path B.

Path A — OAuth capture (current Shopify platform) ⭐

Shopify has retired legacy custom apps on current stores (Spring '26 and later). Apps are now created in the Dev Dashboard, are OAuth-based, and only hand the access token to a backend during install — there's no "reveal token" button anymore.

This repo ships that backend as a one-time command. npm run auth runs a throwaway local callback server, completes the install, and writes the resulting offline access token into stores.json. Offline tokens don't expire, so it's a single step per store — afterwards the MCP server behaves exactly as it would with a legacy static token.

1. Create the app in the Dev Dashboard (Partners → your org → Dev dashboard → Create app → Start from Dev Dashboard).

2. Configure the version: set your Admin API scopes, uncheck Embed app in Shopify admin (this is a headless connector), and add your redirect URL:

http://localhost:3456/callback

3. Set the distribution method. For a single client store, choose Custom distribution and enter that store's .myshopify.com domain. ⚠️ Distribution choice is permanent per app, and custom distribution binds the app to that one store — create a dedicated app per client store.

Custom distribution is what lets you install on a store that is not in your organization — e.g. a client store you only have collaborator access to. It does not work on client-transfer stores held in your own org; see the table above.

4. Configure this repo (stores.json) with the app's Client ID and scopes:

{
  "defaultApiVersion": "2026-07",
  "app": {
    "clientId": "your_client_id",
    "clientSecretEnv": "SHOPIFY_CLIENT_SECRET",
    "redirectUri": "http://localhost:3456/callback",
    "scopes": "read_products,write_products,read_orders,read_content,write_content"
  },
  "stores": {
    "client-a": {
      "label": "Client A (production)",
      "domain": "client-a.myshopify.com",
      "env": "production"
    }
  }
}

5. Put the client secret where the tool can find it. clientSecretEnv names an environment variable; the value belongs in .env (gitignored), which is loaded automatically on startup:

echo 'SHOPIFY_CLIENT_SECRET=shpss_...' >> .env

An existing environment variable always wins over .env, so SHOPIFY_CLIENT_SECRET=… npm run auth … and CI secrets still override the file.

Before this was wired up, .env was never read and npm run auth failed with "App for … has no client secret" even when the secret was sitting in the file. If you see that error, the named variable genuinely isn't reaching the process — check the name in clientSecretEnv matches the key in .env.

6. Run the capture:

npm run build
npm run auth -- --store=client-a

It prints an install URL. Open it in a browser signed into that store's admin, approve the scopes, and the token is written into stores.json automatically.

The callback is verified before any token is stored: HMAC signature (so a forged callback is rejected), state nonce (CSRF), and a shop-domain match against your config.

First install of a custom-distribution app

If Shopify answers "The installation link for this app is invalid" or Oauth error invalid_link, check the store type first: on a client-transfer store no install link will ever work, and you want Path B instead. Otherwise the app uses custom distribution, and its install link is signed by Shopify and cannot be generated locally — neither https://<shop>/admin/oauth/authorize?… nor https://admin.shopify.com/store/<handle>/oauth/install_custom_app?client_id=… will work, however correct they look.

Only the link on the app's own Distribution page works, and it must be copied by hand:

  1. Set the app's App URL to http://localhost:3456/ (Shopify opens it with ?shop=… and the local server answers that handshake). The redirect URL stays http://localhost:3456/callback.

  2. Start the listener and leave it running:

    npm run auth -- --store=client-a --install-link
  3. In the Dev Dashboard / Partners: Apps → your app → Distribution. Press Copy on the Install link, open it in a browser signed into the store admin, and press Install.

Because Shopify originates this flow, the callback carries Shopify's own state rather than a nonce we generated, so --install-link relaxes the state check. HMAC verification and the shop-domain match still apply. After the first install, the ordinary authorize URL works for re-auth.

⚠️ Scopes are fixed at install time. Adding a scope to stores.json later has no effect until you also add it to the app in the Dev Dashboard, release a new version, and re-run the capture. Verify what was actually granted:

curl -s -H "X-Shopify-Access-Token: $TOKEN" \
  https://<shop>.myshopify.com/admin/oauth/access_scopes.json

Path B — legacy custom app

Legacy custom apps mint a static shpat_… token directly in the store admin — no OAuth, no install link, no callback server. Much simpler than Path A when it's available.

As of January 1, 2026, merchants can no longer create them. Two exceptions:

  • Partners can still create them on stores they hold before transfer. This is the sanctioned path for client-transfer stores, and the only one that works there. Creation is disabled once the store transfers to the merchant.

  • Apps created before the cutoff keep working, including their tokens.

Store admin → Settings → Apps → App developmentCreate a legacy custom app. Name it, Configure Admin API scopes, tick what you need, Save, then Install app and reveal the token. Drop it into stores.json:

"woot": {
  "domain": "your-store.myshopify.com",
  "env": "production",
  "adminToken": "shpat_..."
}

A store using adminToken needs no app block — that's only read by npm run auth.

If the page shows only a pointer to the Dev Dashboard with no Create button, the store is merchant-owned and past the cutoff. Use Path A.

Configuration reference

{
  "defaultApiVersion": "2026-07",
  "app": { /* global OAuth app, used by `npm run auth` */ },
  "stores": {
    "alias": {
      "label": "Human-readable name",
      "domain": "store.myshopify.com",
      "env": "production",          // or "dev" — drives the write-gate
      "adminToken": "shpat_...",    // inline token, OR
      "adminTokenEnv": "SOME_VAR",  // read from env, OR
      "apiVersion": "2026-04",      // optional per-store override
      "app": { /* optional per-store app override */ }
    }
  }
}
  • A store with no token is fine — tools return a clear "run npm run auth" error, and list_stores reports authenticated: false.

  • The client secret is only needed for npm run auth, never to run the server.

  • stores.json is gitignored. Never commit real tokens.

Connect it to Claude

Add to your MCP config (Claude Desktop claude_desktop_config.json, or Claude Code .mcp.json) — this wires the official Dev MCP and this multi-store server:

{
  "mcpServers": {
    "shopify-dev": {
      "command": "npx",
      "args": ["-y", "@shopify/dev-mcp@latest"]
    },
    "shopify-multistore": {
      "command": "node",
      "args": ["/absolute/path/to/shopify-multistore-mcp/dist/index.js"],
      "env": {
        "STORES_CONFIG": "/absolute/path/to/shopify-multistore-mcp/stores.json"
      }
    }
  }
}

Or register it once for every project with the Claude Code CLI:

claude mcp add shopify-multistore --scope user \
  --env STORES_CONFIG=$PWD/stores.json \
  -- node $PWD/dist/index.js

If the server fails to start but works fine in your terminal, use an absolute path to node (e.g. /opt/homebrew/bin/node). Desktop apps don't inherit your shell's PATH, so version-manager shims like nvm often aren't on it.

Restart Claude after adding it, then ask "list my shopify stores" to confirm.

For theme/app/Functions work, also install the official Shopify AI Toolkit plugin and the Shopify CLI.

Usage examples

  • "List my configured stores."

  • "Show active products from vendor Acme in client-a."

  • "How many unfulfilled orders does client-a have this week?"

  • "Add a metafield custom.care_instructions to product X in dev-store." (runs)

  • "Update the price of variant Y in client-a." (production — asks to confirm first)

Development

npm run build       # compile to dist/
npm run dev         # tsc --watch
npm run typecheck   # type-check without emitting
npm run auth -- --store=<alias>   # one-time OAuth token capture

Project layout

src/
  index.ts    # MCP server entry (stdio)
  auth.ts     # one-time OAuth token capture (local callback server)
  config.ts   # store registry loader + zod validation
  dotenv.ts   # minimal .env loader (no dependency)
  shopify.ts  # Admin GraphQL client + production write-gate
  tools.ts    # tool definitions
stores.example.json  # template — copy to stores.json (gitignored)

Roadmap

  • Inventory adjustments, theme asset read/write, bulk operations

  • Token refresh / re-auth helper for rotated secrets

  • Optional hosted redirect endpoint (for a permanent, non-localhost install URL)

License

MIT

Related MCP Connectors

Related MCP Servers