Skip to main content
Glama
README.md

```
███████╗██╗  ██╗ ██████╗ ██████╗ ██╗███████╗██╗   ██╗    ███╗   ███╗ ██████╗██████╗ 
██╔════╝██║  ██║██╔═══██╗██╔══██╗██║██╔════╝╚██╗ ██╔╝    ████╗ ████║██╔════╝██╔══██╗
███████╗███████║██║   ██║██████╔╝██║█████╗   ╚████╔╝     ██╔████╔██║██║     ██████╔╝
╚════██║██╔══██║██║   ██║██╔═══╝ ██║██╔══╝    ╚██╔╝      ██║╚██╔╝██║██║     ██╔═══╝ 
███████║██║  ██║╚██████╔╝██║     ██║██║        ██║       ██║ ╚═╝ ██║╚██████╗██║     
╚══════╝╚═╝  ╚═╝ ╚═════╝ ╚═╝     ╚═╝╚═╝        ╚═╝       ╚═╝     ╚═╝ ╚═════╝╚═╝     
```

# Shopify MCP

A Model Context Protocol (MCP) server that connects agents to the Shopify Admin GraphQL API. Use it to browse, edit, and clean up store data via a curated set of tools.

**npm:** `@benwmerritt/shopify-mcp`  
**binary:** `shopify-mcp`

This project started from Ge Li's [shopify-mcp](https://github.com/GeLi2001/shopify-mcp). The unscoped `shopify-mcp` package on npm is theirs and does not include the changes in this repository.

## Highlights

- CRUD for products, collections, orders, and customers
- Draft orders for quotes, manual orders, and B2B pricing
- Inventory and location lookups for stock workflows
- Metafields for custom data
- Metaobject entry creation and lookup for existing definitions
- URL redirects management
- OAuth login flow with local token caching
- Bulk product cleanup utilities
- Fail-closed read-only mode for audit and QA agents

## Prerequisites

- Node.js 20+
- A Shopify custom app (OAuth or Admin API token)

## Local setup (this repo)

Use this when you want to run the MCP server from this local checkout instead of a remote deployment.

1. Install dependencies and build:

```bash
npm install
npm run build
```

2. Create local env config:

```bash
cp .env.example .env
```

Set at least:
- `MYSHOPIFY_DOMAIN=your-store.myshopify.com`
- Either:
  - `SHOPIFY_CLIENT_ID=...` and `SHOPIFY_CLIENT_SECRET=...` for a Dev Dashboard app owned by the store's organization; or
  - `SHOPIFY_ACCESS_TOKEN=shpat_xxx` for a static/manual token
- `REMOTE_MCP=false`

3. Start local MCP (stdio):

```bash
npm run start:local
```

`start:local` uses stdio mode. Remote mode is only enabled with `--remote` or `REMOTE_MCP=true`.

### Read-only mode

Start a capability-restricted server for QA, audit, and reporting agents:

```bash
shopify-mcp --read-only --domain=<YOUR_SHOP>.myshopify.com
# or
SHOPIFY_MCP_READ_ONLY=true npm run start:local
```

Read-only mode exposes only a reviewed allowlist of lookup and reporting tools.
All mutating, mixed read/write, file-upload, and unknown future tools are hidden.
The allowlist is fail-closed, so a newly added tool does not appear in a
read-only instance until it is explicitly reviewed. `get-status` reports the
effective access mode, whether the boundary is enforced, and whether write
tools are exposed.

Use a least-privilege Shopify token as well when one is available. The server
boundary is designed to remain useful when a deployment must temporarily share
an existing token: agents connected to the read-only MCP instance cannot call
the hidden mutation tools.

## Install + run

### Client credentials (same Shopify organization)

For a Dev Dashboard app installed on a store owned by the same Shopify
organization, configure:

```bash
MYSHOPIFY_DOMAIN=your-store.myshopify.com
SHOPIFY_CLIENT_ID=your-client-id
SHOPIFY_CLIENT_SECRET=your-client-secret
```

No callback URL or browser consent is required. The MCP obtains Shopify's
24-hour access token at startup, caches it per permanent MyShopify domain in
`~/.shopify-mcp/tokens.json`, and renews it automatically before expiry.
Cached tokens include the client ID so a token created by a different app is
never silently reused after an app cutover.

Shopify does not allow this grant for public or custom-distribution apps on
stores owned by another organization; use the authorization-code flow below
for those apps.

### Authorization-code OAuth

1. Create a custom app and copy **Client ID** and **Client Secret**.
2. In **App setup**, set **App URL** and **Allowed redirection URLs** to:
   `http://localhost:3456/callback`
3. Start the OAuth flow:

```bash
npx @benwmerritt/shopify-mcp --oauth --domain=your-store.myshopify.com --clientId=xxx --clientSecret=yyy
```

Tokens are stored at `~/.shopify-mcp/tokens.json`. After that, start the server with just the domain:

```bash
npx @benwmerritt/shopify-mcp --domain=your-store.myshopify.com
```

Optional: override scopes with `--scopes` or `SHOPIFY_SCOPES`.

### Access token (manual)

1. Create a custom app in Shopify
2. Enable Admin API scopes:
   - `read_products`, `write_products`
   - `read_customers`, `write_customers`
   - `read_orders`, `write_orders`
   - `read_draft_orders`, `write_draft_orders`
   - `read_inventory`, `write_inventory`
   - `read_locations`
   - `read_content`, `write_content`
   - `read_files`, `write_files`
3. Install the app and copy the Admin API access token

Run:

```bash
shopify-mcp --accessToken=<YOUR_ACCESS_TOKEN> --domain=<YOUR_SHOP>.myshopify.com
```

## MCP client setup

### Claude Desktop (local repo build)

Build first (`npm run build`), then point Claude Desktop at this repo's built entrypoint:

```json
{
  "mcpServers": {
    "shopify-local": {
      "command": "node",
      "args": [
        "/absolute/path/to/shopify-mcp/dist/index.js",
        "--domain",
        "your-store.myshopify.com"
      ],
      "env": {
        "SHOPIFY_ACCESS_TOKEN": "shpat_xxx",
        "REMOTE_MCP": "false"
      }
    }
  }
}
```

If you completed OAuth locally, remove `SHOPIFY_ACCESS_TOKEN` and keep `--domain`.

### Claude Desktop (npm package)

```json
{
  "mcpServers": {
    "shopify": {
      "command": "npx",
      "args": [
        "@benwmerritt/shopify-mcp",
        "--accessToken",
        "<YOUR_ACCESS_TOKEN>",
        "--domain",
        "<YOUR_SHOP>.myshopify.com"
      ]
    }
  }
}
```

If you completed OAuth, omit `--accessToken` and keep `--domain`.

Config paths:
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
- Windows: `%APPDATA%/Claude/claude_desktop_config.json`

### Remote MCP (Railway, etc.)

By default this server runs as a local stdio MCP. Passing `--remote` (or setting
`REMOTE_MCP=true`) switches it to HTTP/SSE mode so it can be deployed as a remote
MCP server for Claude.ai or other remote clients. This repo ships a `Dockerfile`
and `railway.json` so Railway builds and starts it in remote mode out of the box.

**1. Get a token locally (one-time):**
```bash
npx @benwmerritt/shopify-mcp --oauth --domain=your-store.myshopify.com --clientId=xxx --clientSecret=yyy
# Token saved to ~/.shopify-mcp/tokens.json
```

**2. Deploy to Railway:**
- Create a project from this repo. Railway reads `railway.json` and builds the
  `Dockerfile`, which starts the server with `--remote`.
- Set the service environment variables (Railway injects `PORT` automatically):

```bash
SHOPIFY_ACCESS_TOKEN=shpat_xxx           # from tokens.json (or use the OAuth vars)
MYSHOPIFY_DOMAIN=your-store.myshopify.com
MCP_API_KEY=choose-a-long-random-string  # required to authenticate remote clients
# REMOTE_MCP=true is already implied by the Dockerfile's --remote flag
# PORT is injected by Railway (defaults to 3000 when run locally)
```

**3. Connect:**
- Health check: `GET /health`
- Streamable HTTP: `/mcp`, using POST for requests, including MCP 2026-07-28.
- Existing SSE clients: `GET /mcp?apiKey=<MCP_API_KEY>`.
- Existing SSE messages: `POST /messages?apiKey=<MCP_API_KEY>`.

Send `Authorization: Bearer <MCP_API_KEY>` with Streamable HTTP requests.
The existing `apiKey` query parameter also works on both transports. When
`MCP_API_KEY` is configured, missing or invalid credentials receive `401` on
all MCP routes except allowed `OPTIONS` preflights, which do not require credentials.
An explicit Bearer header takes precedence over the query key.
Without `MCP_API_KEY`, development mode binds only to `127.0.0.1`.
External deployments, including containers that publish a port, must configure a
key to listen on all interfaces. The health endpoint remains unauthenticated.
Use HTTPS and configure a key for a public deployment. This shared-key scheme
is not an MCP OAuth authorization server.

MCP routes validate the actual `Host` header before Origin and authentication,
including requests without an `Origin` header and `OPTIONS` preflights. Allowed
hostnames are the configured public app hostname, `localhost`, `127.0.0.1`, and
`[::1]`, with an optional port. Add custom proxy or domain hostnames through
`MCP_ALLOWED_HOSTS`, comma-separated, such as `mcp.example.com,internal-proxy.example.com`.
Use hostnames without schemes or ports; matches are exact and case-insensitive.
`X-Forwarded-Host` is not trusted. Other or missing Host headers receive `403`.
Native clients without an `Origin` header work when their Host is allowed. Browser requests to
MCP routes must use the configured public app origin, the local server origin,
or an exact origin listed in `MCP_ALLOWED_ORIGINS`, comma-separated, such as
`https://client.example,https://another-client.example`. Other origins receive
`403`, including preflight requests. Public app URL resolution continues to use
`PUBLIC_BASE_URL`, `APP_URL`, or the Railway public URL/domain variables.
The upload pages keep their existing URLs and short-lived upload-session checks.

**Test the container locally before deploying:**
```bash
docker build -t shopify-mcp .
docker run -p 3000:3000 \
  -e MYSHOPIFY_DOMAIN=your-store.myshopify.com \
  -e SHOPIFY_ACCESS_TOKEN=shpat_xxx \
  -e MCP_API_KEY=test \
  shopify-mcp
# then in another shell: curl localhost:3000/health
```

### Environment variables (optional)

```bash
SHOPIFY_ACCESS_TOKEN=your_access_token
MYSHOPIFY_DOMAIN=your-store.myshopify.com
# Optional OAuth values:
# SHOPIFY_CLIENT_ID=your_client_id
# SHOPIFY_CLIENT_SECRET=your_client_secret
# SHOPIFY_SCOPES=comma,separated,scopes
# Hide every mutating or unreviewed tool (also available as `--read-only`):
# SHOPIFY_MCP_READ_ONLY=true
```

## Protocol compatibility and upgrade notes

The server uses TypeScript SDK v2 and Zod 4. Local stdio selects the protocol
from the opening message. Remote POST `/mcp` supports both MCP 2026-07-28
and older initialization-based Streamable HTTP clients. Modern requests use
`server/discover` and per-request metadata rather than an initialization session.

Existing stdio commands, Shopify credentials, token renewal, OAuth setup,
read-only flags, tool names, and legacy SSE connection URLs remain available.
The same tool registrations and fail-closed allowlist serve every transport.

Compatibility limits:

- Node 18 is no longer supported by SDK v2. Upgrade Node to 20+ before updating
  this server, or retain the previous release. The Docker image already uses Node 20.
- Legacy SSE uses the SDK's frozen `server-legacy` compatibility package.
  New integrations should use Streamable HTTP; the SSE bridge receives no new features.
- Legacy Streamable HTTP on POST `/mcp` is stateless, with no session ID or replay.
  GET `/mcp` without protocol/session headers remains the original SSE endpoint.
  Streamable HTTP GET and DELETE session operations are unsupported. A reconnect
  to legacy SSE creates a new connection, as before.
- Browser clients from other origins must configure `MCP_ALLOWED_ORIGINS`.
  API-key authentication remains separate from Shopify OAuth and does not provide
  automatic MCP OAuth discovery or token issuance.

Run `npm test -- --runInBand` for the application suite and
`npm run test:connections` for built-server connection and security tests.
Connection tests use SDK v1.17.1 and v2.0.0, explicitly pin modern clients to
2026-07-28, compare tool catalogs, and mock Shopify calls without store access.
See [the research notes](docs/mcp-sdk-v2-research.md) for official sources and migration details.

## Tool catalog

### Products
- `products` — unified lookup/search/filter. Pass `id` for a single product; omit `id` to list/search with filters (`title`, `status`, `vendor`, `tag`, inventory, dates, `hasImages`, …). Returns the product's Shopify Standard Product Taxonomy `category` (`{id, name, fullName}`) in `slim`/`standard`/`full`. Page size capped at 100.
- `create-product`
- `update-product` — accepts `category` (Shopify Standard Product Taxonomy GID, `vp-*` prefix); the tool verifies the category actually stuck and throws a loud, actionable error if Shopify silently rejected the GID, instead of leaving you with a null `category`. Also takes `cost` (cost per item, on the simple form or per entry in `variants`; the response includes `cost` only when one was written, since reading it needs `read_inventory`; without that scope the write still succeeds and the response carries a `warnings` entry instead) and `renameOption: {from, to}` to rename a product option in place without touching variant IDs (send it on its own; it cannot be rolled back if a later write fails).
- `delete-product`
- `delete-variant`
- `delete-product-images`
- `bulk-update-products`
- `bulk-delete-products`
- `count-products-by-tag`
- `find-products-by-metafield` — list products that have / don't have / both for a given `namespace.key`, paginated across the whole catalog via cursor
- `search-taxonomy` — browse Shopify's product category taxonomy; set `includeAttributes:true` to also return each category's attributes (e.g. Color, Pattern) and their allowed values

### Collections
- `get-collections`
- `manage-collection-products`
- `create-collection`
- `update-collection`
- `delete-collection`

### Customers
- `get-customers` (supports pagination via `cursor`)
- `update-customer`

### Orders
- `orders` — unified lookup/list. Pass `id` for a single order; omit `id` to list with filters (`customerId`, `status`, pagination via `cursor`). Replaces `get-orders`, `get-order-by-id`, and `get-customer-orders`.
- `update-order`

### Draft Orders
- `draft-orders` — unified lookup/list. Pass `id` for a single draft order; omit `id` to list with filters (`status`, `query`, pagination via `cursor`).
- `create-draft-order`
- `update-draft-order`
- `complete-draft-order`

### Inventory
- `get-inventory-levels`
- `update-inventory`

### Locations
- `get-locations`

### Metafields
- `get-metafields` — server-side filter with `key`+`namespace` (single field) or `keys: ["namespace.key", …]` (multi) via Shopify's native `metafields(keys:)`; set `includeDefinitions:true` to merge ALL definitions with current values so empty/unfilled fields show up (`value:null`, `isSet:false`)
- `set-metafield` (create or update; supports `metaobject_reference` / `list.metaobject_reference`)
- `bulk-set-variant-metafields` — set metafields across many variants of one product in a single `productVariantsBulkUpdate` call (up to 250 variants/call). UNIFORM mode (`metafields`) fans one value out to every variant and auto-discovers the variant IDs; PER-VARIANT mode (`variants`) sets different values per variant. Avoids one `set-metafield` call per variant.
- `delete-metafield`
- `list-metafield-definitions` — discover metafield definitions for an owner type (PRODUCT, ORDER, CUSTOMER, …); each entry now includes `constraints` (e.g. `{key:"category", values:["vp-2","vp-2-2-3", …]}`) so agents can see category-gating *before* writing (e.g. `vehicle_*` requires `vp-2*` Vehicle categories; values on disallowed categories are silently filtered out by Shopify on read).
- `get-metafield-options` — resolve a metafield's selectable options in one call (for metaobject-reference fields, returns the available metaobject entries; for choice-lists, the allowed choices)

### Metaobjects
- `list-metaobject-definitions`
- `get-metaobject-definition`
- `create-metaobject` — optional `status` (`ACTIVE`/`DRAFT`); defaults to Shopify's `DRAFT` for publishable definitions, pass `ACTIVE` to publish on create
- `update-metaobject` — edit fields on an existing entry (only provided keys change); optional `status` to publish (`ACTIVE`) or unpublish (`DRAFT`)
- `delete-metaobject`
- `list-metaobjects` — returns `status` per entry; optional `status` filter (applied client-side to the fetched page)
- `get-metaobject` — returns the entry's publish `status`

### Files
- `get-files` — list/search files in the store
- `attach-file-to-product` — attach an existing media file to a product
- `detach-file-from-product` — remove a media file from a product
- `create-file-upload-session` — start a browser upload session (**remote mode only**)
- `get-file-upload-session` — check an upload session (**remote mode only**)

### URL redirects
- `get-redirects`
- `create-redirect`
- `delete-redirect`

### Analytics
- `get-store-counts` - Get all key counts in one call (products, variants, orders, customers, collections)
- `get-product-issues` - Audit products for problems (zero inventory, low stock, missing images, zero price)

### Bulk Operations
- `start-bulk-export` - Start async bulk export (products, orders, customers, inventory, or custom query)
- `get-bulk-operation-status` - Check progress of bulk operation
- `get-bulk-operation-results` - Download and parse completed results (summary, sample, or full)

### Server
- `get-status` - Report MCP server status, configured store, and connection health

## Debugging

Tail Claude Desktop logs:

```bash
tail -n 20 -f ~/Library/Logs/Claude/mcp*.log
```

## License

MIT