hasdata-shopify-mcp 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hasdata_shopify_mcp-1.0.0/.github/ISSUE_TEMPLATE/inaccuracy.md +21 -0
- hasdata_shopify_mcp-1.0.0/.github/workflows/contract.yml +31 -0
- hasdata_shopify_mcp-1.0.0/.github/workflows/publish.yml +75 -0
- hasdata_shopify_mcp-1.0.0/.gitignore +11 -0
- hasdata_shopify_mcp-1.0.0/LICENSE +21 -0
- hasdata_shopify_mcp-1.0.0/PKG-INFO +400 -0
- hasdata_shopify_mcp-1.0.0/README.md +386 -0
- hasdata_shopify_mcp-1.0.0/glama.json +7 -0
- hasdata_shopify_mcp-1.0.0/hasdata_shopify_mcp/__init__.py +28 -0
- hasdata_shopify_mcp-1.0.0/index.mjs +24 -0
- hasdata_shopify_mcp-1.0.0/package.json +41 -0
- hasdata_shopify_mcp-1.0.0/pyproject.toml +24 -0
- hasdata_shopify_mcp-1.0.0/server.json +61 -0
- hasdata_shopify_mcp-1.0.0/test/tools.test.mjs +159 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Something in the README is wrong
|
|
3
|
+
about: A tool table, a response sample or a documented behaviour does not match reality
|
|
4
|
+
labels: documentation
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
**Where in the README**
|
|
8
|
+
|
|
9
|
+
Section or heading.
|
|
10
|
+
|
|
11
|
+
**What it says**
|
|
12
|
+
|
|
13
|
+
Quote the line.
|
|
14
|
+
|
|
15
|
+
**The call you made**
|
|
16
|
+
|
|
17
|
+
Tool name and arguments, or the equivalent REST URL with your key removed.
|
|
18
|
+
|
|
19
|
+
**What came back**
|
|
20
|
+
|
|
21
|
+
Trimmed response, with anything private removed.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# The tool contract is checked on a schedule as well as on push, because the upstream tool list
|
|
2
|
+
# can change without a single commit in this repository.
|
|
3
|
+
name: tool contract
|
|
4
|
+
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
branches: [main]
|
|
8
|
+
pull_request:
|
|
9
|
+
schedule:
|
|
10
|
+
- cron: '0 6 * * 1'
|
|
11
|
+
workflow_dispatch:
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
contract:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
timeout-minutes: 5
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
- uses: actions/setup-node@v4
|
|
23
|
+
with:
|
|
24
|
+
node-version: '22'
|
|
25
|
+
# Forks cannot read repository secrets. The suite skips its live checks when the key is
|
|
26
|
+
# absent, so a pull request from a fork stays green instead of failing for a reason the
|
|
27
|
+
# contributor cannot fix.
|
|
28
|
+
- name: Assert the tool list still matches the README
|
|
29
|
+
env:
|
|
30
|
+
HASDATA_API_KEY: ${{ secrets.HASDATA_API_KEY }}
|
|
31
|
+
run: npm test
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Publishes the npm and PyPI wrapper packages on a version tag, using OIDC
|
|
2
|
+
# trusted publishing. No NPM_TOKEN or PYPI_TOKEN is stored anywhere: GitHub
|
|
3
|
+
# mints a short-lived OIDC token per run, and npmjs.org / pypi.org accept it
|
|
4
|
+
# because this repo + workflow are configured as trusted publishers.
|
|
5
|
+
#
|
|
6
|
+
# One-time setup, done once per package on the registries (not in this repo):
|
|
7
|
+
# npmjs.org -> package settings -> Trusted Publisher -> GitHub Actions,
|
|
8
|
+
# repo HasData/shopify-mcp, workflow publish.yml
|
|
9
|
+
# pypi.org -> the hasdata org -> Publishing -> add a trusted publisher
|
|
10
|
+
# (pending publisher works before the first release),
|
|
11
|
+
# repo HasData/shopify-mcp, workflow publish.yml
|
|
12
|
+
#
|
|
13
|
+
# The MCP registry entry (com.hasdata/shopify) is NOT published here. It uses
|
|
14
|
+
# domain auth, which would need the namespace-wide Ed25519 key as a secret in
|
|
15
|
+
# every repo. That key stays off CI; the registry entry is published by hand
|
|
16
|
+
# when server.json changes, after the package versions below are live.
|
|
17
|
+
#
|
|
18
|
+
# Release: bump nothing by hand. Tag the commit `vX.Y.Z` and push the tag; the
|
|
19
|
+
# tag is the single source of the version and is written into both manifests.
|
|
20
|
+
|
|
21
|
+
name: publish
|
|
22
|
+
|
|
23
|
+
on:
|
|
24
|
+
push:
|
|
25
|
+
tags: ['v*.*.*']
|
|
26
|
+
|
|
27
|
+
permissions:
|
|
28
|
+
contents: read
|
|
29
|
+
id-token: write
|
|
30
|
+
|
|
31
|
+
jobs:
|
|
32
|
+
npm:
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/checkout@v4
|
|
36
|
+
# No registry-url here on purpose. With it, setup-node writes an .npmrc
|
|
37
|
+
# carrying _authToken=${NODE_AUTH_TOKEN}, which resolves to a placeholder
|
|
38
|
+
# when no token is passed. npm then authenticates with that garbage instead
|
|
39
|
+
# of falling back to OIDC, and a scoped package answers 404.
|
|
40
|
+
- uses: actions/setup-node@v4
|
|
41
|
+
with:
|
|
42
|
+
node-version: '24'
|
|
43
|
+
# OIDC trusted publishing landed in npm 11.5.1. Node 24 already ships a
|
|
44
|
+
# newer npm than that, but pinning the upgrade here keeps the job working
|
|
45
|
+
# if the runner image drifts back.
|
|
46
|
+
- name: Upgrade npm for OIDC trusted publishing
|
|
47
|
+
run: |
|
|
48
|
+
npm install -g npm@latest
|
|
49
|
+
npm -v
|
|
50
|
+
- name: Set version from the tag
|
|
51
|
+
run: npm version "${GITHUB_REF_NAME#v}" --no-git-tag-version --allow-same-version
|
|
52
|
+
- name: Publish to npm (OIDC, no token)
|
|
53
|
+
run: npm publish --access public
|
|
54
|
+
|
|
55
|
+
pypi:
|
|
56
|
+
runs-on: ubuntu-latest
|
|
57
|
+
steps:
|
|
58
|
+
- uses: actions/checkout@v4
|
|
59
|
+
- uses: actions/setup-python@v5
|
|
60
|
+
with:
|
|
61
|
+
python-version: '3.12'
|
|
62
|
+
- name: Set version from the tag
|
|
63
|
+
run: |
|
|
64
|
+
python - "${GITHUB_REF_NAME#v}" <<'PY'
|
|
65
|
+
import re, sys
|
|
66
|
+
v = sys.argv[1]
|
|
67
|
+
p = "pyproject.toml"
|
|
68
|
+
t = open(p, encoding="utf-8").read()
|
|
69
|
+
t = re.sub(r'(?m)^version = ".*"$', f'version = "{v}"', t, count=1)
|
|
70
|
+
open(p, "w", encoding="utf-8", newline="\n").write(t)
|
|
71
|
+
PY
|
|
72
|
+
- name: Build the wheel and sdist
|
|
73
|
+
run: pipx run build
|
|
74
|
+
- name: Publish to PyPI (OIDC, no token)
|
|
75
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 HasData
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: hasdata-shopify-mcp
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: MCP server for Shopify through HasData's hosted API. 1,000 free credits every month.
|
|
5
|
+
Project-URL: Homepage, https://hasdata.com/apis/shopify-api
|
|
6
|
+
Project-URL: Repository, https://github.com/HasData/shopify-mcp
|
|
7
|
+
License: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: ecommerce,hasdata,mcp,model-context-protocol,price-monitoring,product-data,shopify
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Requires-Dist: mcp-proxy>=0.12.0
|
|
12
|
+
Requires-Dist: mcp<2,>=1.17
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# Shopify MCP Server
|
|
16
|
+
|
|
17
|
+
<!-- mcp-name: com.hasdata/shopify -->
|
|
18
|
+
|
|
19
|
+
A hosted Model Context Protocol (MCP) server that gives Claude, Cursor, Windsurf and any other MCP client two read-only Shopify tools. Pull the product catalogue of any public Shopify storefront by its URL, with variants, SKUs and prices, and list the collections that organise it, all as structured JSON, with no app to install and no merchant token.
|
|
20
|
+
|
|
21
|
+
It reads the catalogue a signed-out visitor can see, on any classic Shopify storefront, whether it sits on a `myshopify.com` address or a custom domain. A headless store answers on its `myshopify.com` domain.
|
|
22
|
+
|
|
23
|
+
**1,000 free credits every month, no card required**, which is 200 Shopify calls at the 5-credit rate.
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
https://mcp.hasdata.com/api/mcp?apis=shopify
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
[](https://glama.ai/mcp/servers/HasData/shopify-mcp)
|
|
30
|
+
[](https://github.com/HasData/shopify-mcp/actions/workflows/contract.yml)
|
|
31
|
+
[](https://mcp.hasdata.com/api/mcp?apis=shopify)
|
|
32
|
+
[](#tools)
|
|
33
|
+
[](https://www.npmjs.com/package/@hasdata/shopify-mcp)
|
|
34
|
+
[](https://pypi.org/project/hasdata-shopify-mcp/)
|
|
35
|
+
[](LICENSE)
|
|
36
|
+
|
|
37
|
+
## Contents
|
|
38
|
+
|
|
39
|
+
- [What you need](#what-you-need)
|
|
40
|
+
- [Quick start](#quick-start)
|
|
41
|
+
- [Example prompts](#example-prompts)
|
|
42
|
+
- [Tools](#tools)
|
|
43
|
+
- [Errors and failure paths](#errors-and-failure-paths)
|
|
44
|
+
- [Pricing, free tier and limits](#pricing-free-tier-and-limits)
|
|
45
|
+
- [Tool selection](#tool-selection)
|
|
46
|
+
- [How it compares](#how-it-compares)
|
|
47
|
+
- [FAQ](#faq)
|
|
48
|
+
- [HasData links](#hasdata-links)
|
|
49
|
+
- [Development](#development)
|
|
50
|
+
- [Contributing](#contributing)
|
|
51
|
+
- [License](#license)
|
|
52
|
+
|
|
53
|
+
## What you need
|
|
54
|
+
|
|
55
|
+
An MCP client and a HasData API key from the [dashboard](https://app.hasdata.com/sign-up?utm_source=github&utm_medium=syndication&utm_campaign=shopify-mcp), free to create with no card, and the free tier covers about 200 calls a month at the 5-credit rate. This is a remote server, so the simplest path is a URL and an `x-api-key` header, with no container to run. A client that only speaks stdio reaches it through a thin launcher, published as `@hasdata/shopify-mcp` on npm and `hasdata-shopify-mcp` on PyPI, shown below.
|
|
56
|
+
|
|
57
|
+
## Quick start
|
|
58
|
+
|
|
59
|
+
The server URL is the same for every client. We run it hands-on in Claude Code and Claude Desktop. The other blocks follow each client's own documented format for a remote server.
|
|
60
|
+
|
|
61
|
+
| Field | Value |
|
|
62
|
+
| :--- | :--- |
|
|
63
|
+
| URL | `https://mcp.hasdata.com/api/mcp?apis=shopify` |
|
|
64
|
+
| Transport | HTTP, streamable |
|
|
65
|
+
| Auth header | `x-api-key: HASDATA_API_KEY` |
|
|
66
|
+
|
|
67
|
+
Clients with OAuth support can add the same URL as a connector and sign in without putting a key in a config file.
|
|
68
|
+
|
|
69
|
+
<details>
|
|
70
|
+
<summary><b>Claude Code</b></summary>
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
claude mcp add --transport http shopify "https://mcp.hasdata.com/api/mcp?apis=shopify" \
|
|
74
|
+
--header "x-api-key: HASDATA_API_KEY"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
</details>
|
|
78
|
+
|
|
79
|
+
<details>
|
|
80
|
+
<summary><b>Claude Desktop</b></summary>
|
|
81
|
+
|
|
82
|
+
Settings, then Connectors, then Add custom connector, then paste `https://mcp.hasdata.com/api/mcp?apis=shopify` and sign in.
|
|
83
|
+
|
|
84
|
+
For the config-file route, Claude Desktop loads only local (stdio) servers, so it reaches a remote server through a stdio launcher. The `@hasdata/shopify-mcp` package is that launcher, and it reads the key from the environment. Add this to `claude_desktop_config.json`:
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"mcpServers": {
|
|
89
|
+
"shopify": {
|
|
90
|
+
"command": "npx",
|
|
91
|
+
"args": ["-y", "@hasdata/shopify-mcp"],
|
|
92
|
+
"env": { "HASDATA_API_KEY": "YOUR_KEY" }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
For Python instead of Node, swap the launcher for the PyPI package, which `uvx` runs without a manual install:
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{
|
|
102
|
+
"mcpServers": {
|
|
103
|
+
"shopify": {
|
|
104
|
+
"command": "uvx",
|
|
105
|
+
"args": ["hasdata-shopify-mcp"],
|
|
106
|
+
"env": { "HASDATA_API_KEY": "YOUR_KEY" }
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
</details>
|
|
113
|
+
|
|
114
|
+
<details>
|
|
115
|
+
<summary><b>Cursor</b></summary>
|
|
116
|
+
|
|
117
|
+
`~/.cursor/mcp.json` for every project, or `.cursor/mcp.json` for one:
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"mcpServers": {
|
|
122
|
+
"shopify": {
|
|
123
|
+
"url": "https://mcp.hasdata.com/api/mcp?apis=shopify",
|
|
124
|
+
"headers": { "x-api-key": "HASDATA_API_KEY" }
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
</details>
|
|
131
|
+
|
|
132
|
+
<details>
|
|
133
|
+
<summary><b>Windsurf</b></summary>
|
|
134
|
+
|
|
135
|
+
`~/.codeium/windsurf/mcp_config.json`. Windsurf calls the field `serverUrl`, not `url`:
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"mcpServers": {
|
|
140
|
+
"shopify": {
|
|
141
|
+
"serverUrl": "https://mcp.hasdata.com/api/mcp?apis=shopify",
|
|
142
|
+
"headers": { "x-api-key": "HASDATA_API_KEY" }
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
</details>
|
|
149
|
+
|
|
150
|
+
<details>
|
|
151
|
+
<summary><b>VS Code</b></summary>
|
|
152
|
+
|
|
153
|
+
`.vscode/mcp.json` in the workspace:
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
{
|
|
157
|
+
"servers": {
|
|
158
|
+
"shopify": {
|
|
159
|
+
"type": "http",
|
|
160
|
+
"url": "https://mcp.hasdata.com/api/mcp?apis=shopify",
|
|
161
|
+
"headers": { "x-api-key": "HASDATA_API_KEY" }
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
</details>
|
|
168
|
+
|
|
169
|
+
## Example prompts
|
|
170
|
+
|
|
171
|
+
Each of these lands on one tool, or on two in sequence when the second needs a handle the first returns.
|
|
172
|
+
|
|
173
|
+
- List the collections on allbirds.com and tell me which ones hold the most products.
|
|
174
|
+
- Pull the first 250 products from this store and group them by `product_type`.
|
|
175
|
+
- Which variants in this store are out of stock right now?
|
|
176
|
+
- Find every product on this store that is discounted, comparing `price` against `compare_at_price`.
|
|
177
|
+
- Page through the shoes collection on this storefront and give me the price range per size.
|
|
178
|
+
- Compare the sock prices on these two Shopify stores.
|
|
179
|
+
|
|
180
|
+
A prompt that names a category rather than a handle takes two calls, one to list the collections and one to pull the products in the matching handle. The collection tool returns `handle`, and that value goes straight into the `collection` argument of the product tool.
|
|
181
|
+
|
|
182
|
+
## Tools
|
|
183
|
+
|
|
184
|
+
Two tools, 5 credits per successful call. Both take a storefront URL and page through the results with `limit` and `page`, where `limit` accepts up to 250.
|
|
185
|
+
|
|
186
|
+
### Get Shopify store products
|
|
187
|
+
|
|
188
|
+
[`hasdata_shopify_products_getProducts`](https://docs.hasdata.com/apis/shopify/products?utm_source=github&utm_medium=syndication&utm_campaign=shopify-mcp)
|
|
189
|
+
|
|
190
|
+
A page of products from one storefront.
|
|
191
|
+
|
|
192
|
+
| Parameter | Type | Required | Notes |
|
|
193
|
+
| :--- | :--- | :--- | :--- |
|
|
194
|
+
| `url` | string | yes | The storefront, such as `https://www.allbirds.com` |
|
|
195
|
+
| `limit` | number | | Products per page, 1 to 250 |
|
|
196
|
+
| `page` | number | | Page number, starting at 1 |
|
|
197
|
+
| `collection` | string | | Restrict to one collection, by its handle |
|
|
198
|
+
|
|
199
|
+
Returns a `products` array. Each product carries `id`, `title`, `handle`, `body_html`, `vendor`, `product_type`, `tags`, `published_at`, `created_at`, `updated_at`, an `options` array naming the axes the variants vary on, an `images` array, and a `variants` array.
|
|
200
|
+
|
|
201
|
+
The price lives on the variant, never on the product. A variant carries `id`, `title`, `sku`, `price`, `compare_at_price`, `available`, `grams`, `position`, `requires_shipping`, `taxable`, the `option1` through `option3` values and its own timestamps.
|
|
202
|
+
|
|
203
|
+
```json
|
|
204
|
+
{
|
|
205
|
+
"id": 6889962537040,
|
|
206
|
+
"title": "Anytime Ankle Sock - Basin Blue",
|
|
207
|
+
"handle": "anytime-ankle-sock-basin-blue",
|
|
208
|
+
"vendor": "Allbirds",
|
|
209
|
+
"product_type": "Socks",
|
|
210
|
+
"updated_at": "2026-09-09T05:25:09-07:00",
|
|
211
|
+
"options": [{ "name": "Size", "position": 1, "values": ["S (W5-7)", "M (W8-10 / M8)", "L (W11 / M9-12)", "XL (M13-14)"] }],
|
|
212
|
+
"variants": [
|
|
213
|
+
{
|
|
214
|
+
"id": 40356485202000,
|
|
215
|
+
"title": "S (W5-7)",
|
|
216
|
+
"sku": "A10842U001",
|
|
217
|
+
"price": "16.00",
|
|
218
|
+
"compare_at_price": null,
|
|
219
|
+
"available": false,
|
|
220
|
+
"grams": 59,
|
|
221
|
+
"position": 1
|
|
222
|
+
}
|
|
223
|
+
],
|
|
224
|
+
"images": [
|
|
225
|
+
{
|
|
226
|
+
"id": 36355227844688,
|
|
227
|
+
"position": 1,
|
|
228
|
+
"src": "https://cdn.shopify.com/s/files/1/1104/4168/files/A10842_S24Q1_Anytime_Ankle_Sock_Basin_Blue_A-1400x1400.png?v=1776183348",
|
|
229
|
+
"width": 1400,
|
|
230
|
+
"height": 1400
|
|
231
|
+
}
|
|
232
|
+
]
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Get Shopify store collections
|
|
237
|
+
|
|
238
|
+
[`hasdata_shopify_collections_getCollections`](https://docs.hasdata.com/apis/shopify/collections?utm_source=github&utm_medium=syndication&utm_campaign=shopify-mcp)
|
|
239
|
+
|
|
240
|
+
The collections that organise one storefront.
|
|
241
|
+
|
|
242
|
+
| Parameter | Type | Required | Notes |
|
|
243
|
+
| :--- | :--- | :--- | :--- |
|
|
244
|
+
| `url` | string | yes | The storefront, such as `https://www.allbirds.com` |
|
|
245
|
+
| `limit` | number | | Collections per page, 1 to 250 |
|
|
246
|
+
| `page` | number | | Page number, starting at 1 |
|
|
247
|
+
|
|
248
|
+
Returns a `collections` array. Each entry carries `id`, `title`, `handle`, `description`, `image`, `products_count`, `published_at` and `updated_at`.
|
|
249
|
+
|
|
250
|
+
This is the merchandising taxonomy as the store publishes it, which makes it the cheapest way to see how a competitor groups a catalogue before pulling any products. The `handle` is the join key into the product tool.
|
|
251
|
+
|
|
252
|
+
```json
|
|
253
|
+
{
|
|
254
|
+
"id": 135995326544,
|
|
255
|
+
"title": "Accessories",
|
|
256
|
+
"handle": "womens-accessories",
|
|
257
|
+
"description": "You know what they say: It's all in the details. Customize your look with planet-friendly face masks, hats, and more. ",
|
|
258
|
+
"published_at": "2019-08-05T14:01:17-07:00",
|
|
259
|
+
"updated_at": "2026-07-08T13:39:17-07:00",
|
|
260
|
+
"image": null,
|
|
261
|
+
"products_count": 28
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Errors and failure paths
|
|
266
|
+
|
|
267
|
+
Plan for these rather than assuming a happy path.
|
|
268
|
+
|
|
269
|
+
**`price` and `compare_at_price` are strings, not numbers.** They arrive as `"16.00"`, exactly as the storefront publishes them. Convert before you compare or sum, because string ordering puts `"9.00"` above `"16.00"`.
|
|
270
|
+
|
|
271
|
+
**A product has no price of its own.** Anything about cost has to go through the `variants` array, and a product with a size or colour axis usually has several prices. Reading the first variant and calling it the price is the most common mistake here.
|
|
272
|
+
|
|
273
|
+
**`compare_at_price` is null when nothing is discounted**, so a discount check is a null test first and a comparison second.
|
|
274
|
+
|
|
275
|
+
**`available` is per variant and reflects the moment of the call.** A product is not out of stock, a variant is, and stock moves. Two calls minutes apart can disagree, which is the point when you are monitoring, and a trap when you are diffing catalogues.
|
|
276
|
+
|
|
277
|
+
**A collection can report `products_count` of zero.** Stores leave empty, staged and seasonal collections published, so an empty collection is normal rather than a failed call.
|
|
278
|
+
|
|
279
|
+
**Stores publish things that are not for sale.** Internal, retired and staging items sit in the public catalogue on plenty of stores, sometimes flagged in the title and sometimes not. Filter on what you actually need instead of trusting that every row is a live product.
|
|
280
|
+
|
|
281
|
+
**`tags` are whatever the merchant wrote.** Some stores use them as plain keywords, others push namespaced metafield strings into them. Treat the array as free text.
|
|
282
|
+
|
|
283
|
+
**`body_html` is HTML.** Strip it before you index or embed the description.
|
|
284
|
+
|
|
285
|
+
**`collection.image` is often null**, and so is `featured_image` on a variant. Fall back to the product `images` array.
|
|
286
|
+
|
|
287
|
+
**A URL that is not a classic Shopify storefront still answers 200 and still bills.** There is no `products` array in that response and an `error` string in its place, while `requestMetadata.status` stays `ok`. Test for the array before you read it, because the shape changes rather than the status.
|
|
288
|
+
|
|
289
|
+
**A headless Shopify store fails on its custom domain and works on its `myshopify.com` one.** Headless shops serve the storefront from their own front end, so the catalogue is not published under the public domain. When a store you know runs Shopify comes back with the `error` string, retry it as `https://<shop>.myshopify.com`.
|
|
290
|
+
|
|
291
|
+
Results that carry data also carry a `requestMetadata.id` worth quoting in support.
|
|
292
|
+
|
|
293
|
+
## Pricing, free tier and limits
|
|
294
|
+
|
|
295
|
+
Each Shopify tool costs **5 credits per successful call**. Response size does not change the price, so a 250-product page and a 3-product page cost the same, which makes the largest page the cheapest way to mirror a catalogue.
|
|
296
|
+
|
|
297
|
+
The free tier is **1,000 credits every month with no card**, which is 200 Shopify calls at the base rate. It renews with the billing cycle, so a low-volume agent runs on the free tier indefinitely.
|
|
298
|
+
|
|
299
|
+
Paid plans start at **$49 a month** for 200,000 credits, which is 40,000 calls. The unit price falls with volume, from **$1.23 per 1,000 calls** on the entry plan to **$0.50** on Business, **$0.42** on Growth and **$0.37** on the largest [high-volume plans](https://hasdata.com/prices?utm_source=github&utm_medium=syndication&utm_campaign=shopify-mcp).
|
|
300
|
+
|
|
301
|
+
Your plan also sets concurrency. The free tier allows 1 request at a time, Startup 15, Business 30, Growth 50, and the high-volume plans run from 200 to 1,500. Handle the overflow case defensively in anything unattended, because an agent that fans out across stores will reach the ceiling before you do.
|
|
302
|
+
|
|
303
|
+
A request that comes back non-200 is not billed. A successful call that finds nothing is still a call.
|
|
304
|
+
|
|
305
|
+
## Tool selection
|
|
306
|
+
|
|
307
|
+
Start from what the prompt gives you. A question about the catalogue itself goes to the product tool. A question about how the store is organised, or a prompt that names a category by its shop-facing name, goes to the collection tool first.
|
|
308
|
+
|
|
309
|
+
Then think about page size. Both tools accept `limit` up to 250 and cost the same at any size, so a catalogue of 900 products is four calls, not ninety. Leaving `limit` at its default is the most expensive habit you can pick up here.
|
|
310
|
+
|
|
311
|
+
Filter server-side when you can. Passing `collection` to the product tool costs one call and returns the subset, where pulling the whole catalogue and filtering locally costs one call per page of everything you did not want.
|
|
312
|
+
|
|
313
|
+
## How it compares
|
|
314
|
+
|
|
315
|
+
Shopify's Admin API is the official route to a store's catalogue, and it answers a different question.
|
|
316
|
+
|
|
317
|
+
| | Shopify Admin API | This server |
|
|
318
|
+
| :--- | :--- | :--- |
|
|
319
|
+
| Which stores | The ones you own or were granted access to | Any public storefront |
|
|
320
|
+
| Setup | Create an app, request scopes, hold a token per store | One header |
|
|
321
|
+
| Credential per store | Yes | No |
|
|
322
|
+
| Inventory levels | Exact counts | An `available` flag per variant |
|
|
323
|
+
| Draft and hidden products | Returned | Not returned, they are not public |
|
|
324
|
+
| Orders and customers | Returned | Not returned |
|
|
325
|
+
| Cost | Free within rate limits | Paid past the free tier, 5 credits a call |
|
|
326
|
+
|
|
327
|
+
The row that decides it is which stores. The Admin API is built for a merchant working on their own shop, and it needs a token that only that merchant can issue, which rules it out for comparing yourself against ten competitors. When the store is yours, the Admin API is more complete and free, and you should use it.
|
|
328
|
+
|
|
329
|
+
## FAQ
|
|
330
|
+
|
|
331
|
+
### Does Shopify have its own MCP server?
|
|
332
|
+
|
|
333
|
+
Yes, and it does something else. Every eligible storefront exposes one at its own domain, and its tools are built for an agent that is shopping, such as searching the catalogue, building a cart and running a checkout. It is per-store, so an agent comparing thirty shops needs thirty connections. This server is for reading catalogues in bulk across arbitrary stores, so the two do not overlap. If your agent is buying from one shop, use Shopify's.
|
|
334
|
+
|
|
335
|
+
### What is a Shopify MCP server?
|
|
336
|
+
|
|
337
|
+
An MCP server exposes tools an AI client can call. This one turns the public catalogue of any Shopify storefront into JSON an agent can reason over, without a browser or a scraping library in your stack.
|
|
338
|
+
|
|
339
|
+
### Do I need a Shopify account, an app or a merchant token?
|
|
340
|
+
|
|
341
|
+
No. The only credential is your HasData key.
|
|
342
|
+
|
|
343
|
+
### Does it work on custom domains?
|
|
344
|
+
|
|
345
|
+
For a classic storefront, yes, and a store on its own domain is the same as one on `myshopify.com`. A headless store is the exception. Its front end is served by something other than Shopify, so the catalogue is not published under the custom domain and the call comes back empty. Retry those as `https://<shop>.myshopify.com`.
|
|
346
|
+
|
|
347
|
+
### How do I tell whether a site runs Shopify?
|
|
348
|
+
|
|
349
|
+
Call the product tool on it, then look at the shape rather than the status. A classic Shopify storefront answers with a `products` array. Anything else answers 200 with an `error` string and no array, and that response is billed like any other successful call.
|
|
350
|
+
|
|
351
|
+
### Can I get inventory counts?
|
|
352
|
+
|
|
353
|
+
No, only the `available` flag each variant publishes. Exact stock levels are not public, and they come from the Admin API on a store you control.
|
|
354
|
+
|
|
355
|
+
### How do I pull a whole catalogue?
|
|
356
|
+
|
|
357
|
+
Page with `limit` at 250 and step `page` until a page comes back short or empty. Cost scales with pages, not with products, so the largest page size is always the cheapest route.
|
|
358
|
+
|
|
359
|
+
### Can I use this together with other HasData APIs?
|
|
360
|
+
|
|
361
|
+
Yes. One key covers everything, and one endpoint serves them all through the `apis` parameter. Point a client at `?apis=shopify,amazon` to get both tool sets in one connection, or at [`mcp.hasdata.com/api/mcp`](https://docs.hasdata.com/mcp-server?utm_source=github&utm_medium=syndication&utm_campaign=shopify-mcp) for the full catalogue.
|
|
362
|
+
|
|
363
|
+
### Is HasData affiliated with Shopify?
|
|
364
|
+
|
|
365
|
+
No. HasData is an independent service and is not affiliated with, endorsed by, or sponsored by Shopify. Shopify is a trademark of its respective owner. The tools work with publicly available data only, and you are responsible for using the results in line with the terms of the stores you read and the law that applies to you.
|
|
366
|
+
|
|
367
|
+
### Compliance and personal data
|
|
368
|
+
|
|
369
|
+
A product catalogue is business data, and these tools return no customer, order or contact information. The `vendor` field can carry a sole trader's own name on a small store, which is the one place a person can appear. Storing a competitor's catalogue is a commercial decision rather than a privacy one, so read the terms of the store you are pulling from and check your own obligations.
|
|
370
|
+
|
|
371
|
+
## HasData links
|
|
372
|
+
|
|
373
|
+
- [Shopify Scraper API](https://hasdata.com/apis/shopify-api?utm_source=github&utm_medium=syndication&utm_campaign=shopify-mcp), the REST endpoints behind these tools
|
|
374
|
+
- [API documentation](https://docs.hasdata.com/apis/shopify/products?utm_source=github&utm_medium=syndication&utm_campaign=shopify-mcp)
|
|
375
|
+
- [MCP server documentation](https://docs.hasdata.com/mcp-server?utm_source=github&utm_medium=syndication&utm_campaign=shopify-mcp)
|
|
376
|
+
- [Pricing](https://hasdata.com/prices?utm_source=github&utm_medium=syndication&utm_campaign=shopify-mcp)
|
|
377
|
+
- [Dashboard](https://app.hasdata.com/sign-up?utm_source=github&utm_medium=syndication&utm_campaign=shopify-mcp)
|
|
378
|
+
|
|
379
|
+
Other HasData MCP servers: [Google Search](https://github.com/HasData/google-search-mcp), [Google Maps](https://github.com/HasData/google-maps-mcp), [Google Trends](https://github.com/HasData/google-trends-mcp), [Google Flights](https://github.com/HasData/google-flights-mcp), [DuckDuckGo](https://github.com/HasData/duckduckgo-mcp), [YouTube](https://github.com/HasData/youtube-mcp), [TikTok](https://github.com/HasData/tiktok-mcp), [Instagram](https://github.com/HasData/instagram-mcp), [Amazon](https://github.com/HasData/amazon-mcp), [Yelp](https://github.com/HasData/yelp-mcp), [Zillow](https://github.com/HasData/zillow-mcp), [Airbnb](https://github.com/HasData/airbnb-mcp), [Booking.com](https://github.com/HasData/booking-mcp), [Indeed](https://github.com/HasData/indeed-mcp).
|
|
380
|
+
|
|
381
|
+
## Development
|
|
382
|
+
|
|
383
|
+
The launcher is a thin stdio bridge to the remote server, so there is nothing to build.
|
|
384
|
+
|
|
385
|
+
```bash
|
|
386
|
+
npm install
|
|
387
|
+
HASDATA_API_KEY=your_key_here npm test
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
The tests in `test/` assert the tool contract, the part that can break without a commit here. They check that `?apis=shopify` returns the expected tool count, that no name changed, that every tool still declares its required parameter and carries a description, and that the key in use is actually accepted. That last check calls a tool for real and costs 5 credits, which is the price of a canary that can fail for the right reason.
|
|
391
|
+
|
|
392
|
+
The contract suite also runs weekly on a schedule, because the upstream tool list can change without anyone touching this repository.
|
|
393
|
+
|
|
394
|
+
## Contributing
|
|
395
|
+
|
|
396
|
+
A tool table, a response sample or a documented behaviour that does not match reality is worth an issue. There is a template for exactly that. Pull requests are welcome for the same, and for anything in the launcher.
|
|
397
|
+
|
|
398
|
+
## License
|
|
399
|
+
|
|
400
|
+
MIT, see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
# Shopify MCP Server
|
|
2
|
+
|
|
3
|
+
<!-- mcp-name: com.hasdata/shopify -->
|
|
4
|
+
|
|
5
|
+
A hosted Model Context Protocol (MCP) server that gives Claude, Cursor, Windsurf and any other MCP client two read-only Shopify tools. Pull the product catalogue of any public Shopify storefront by its URL, with variants, SKUs and prices, and list the collections that organise it, all as structured JSON, with no app to install and no merchant token.
|
|
6
|
+
|
|
7
|
+
It reads the catalogue a signed-out visitor can see, on any classic Shopify storefront, whether it sits on a `myshopify.com` address or a custom domain. A headless store answers on its `myshopify.com` domain.
|
|
8
|
+
|
|
9
|
+
**1,000 free credits every month, no card required**, which is 200 Shopify calls at the 5-credit rate.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
https://mcp.hasdata.com/api/mcp?apis=shopify
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
[](https://glama.ai/mcp/servers/HasData/shopify-mcp)
|
|
16
|
+
[](https://github.com/HasData/shopify-mcp/actions/workflows/contract.yml)
|
|
17
|
+
[](https://mcp.hasdata.com/api/mcp?apis=shopify)
|
|
18
|
+
[](#tools)
|
|
19
|
+
[](https://www.npmjs.com/package/@hasdata/shopify-mcp)
|
|
20
|
+
[](https://pypi.org/project/hasdata-shopify-mcp/)
|
|
21
|
+
[](LICENSE)
|
|
22
|
+
|
|
23
|
+
## Contents
|
|
24
|
+
|
|
25
|
+
- [What you need](#what-you-need)
|
|
26
|
+
- [Quick start](#quick-start)
|
|
27
|
+
- [Example prompts](#example-prompts)
|
|
28
|
+
- [Tools](#tools)
|
|
29
|
+
- [Errors and failure paths](#errors-and-failure-paths)
|
|
30
|
+
- [Pricing, free tier and limits](#pricing-free-tier-and-limits)
|
|
31
|
+
- [Tool selection](#tool-selection)
|
|
32
|
+
- [How it compares](#how-it-compares)
|
|
33
|
+
- [FAQ](#faq)
|
|
34
|
+
- [HasData links](#hasdata-links)
|
|
35
|
+
- [Development](#development)
|
|
36
|
+
- [Contributing](#contributing)
|
|
37
|
+
- [License](#license)
|
|
38
|
+
|
|
39
|
+
## What you need
|
|
40
|
+
|
|
41
|
+
An MCP client and a HasData API key from the [dashboard](https://app.hasdata.com/sign-up?utm_source=github&utm_medium=syndication&utm_campaign=shopify-mcp), free to create with no card, and the free tier covers about 200 calls a month at the 5-credit rate. This is a remote server, so the simplest path is a URL and an `x-api-key` header, with no container to run. A client that only speaks stdio reaches it through a thin launcher, published as `@hasdata/shopify-mcp` on npm and `hasdata-shopify-mcp` on PyPI, shown below.
|
|
42
|
+
|
|
43
|
+
## Quick start
|
|
44
|
+
|
|
45
|
+
The server URL is the same for every client. We run it hands-on in Claude Code and Claude Desktop. The other blocks follow each client's own documented format for a remote server.
|
|
46
|
+
|
|
47
|
+
| Field | Value |
|
|
48
|
+
| :--- | :--- |
|
|
49
|
+
| URL | `https://mcp.hasdata.com/api/mcp?apis=shopify` |
|
|
50
|
+
| Transport | HTTP, streamable |
|
|
51
|
+
| Auth header | `x-api-key: HASDATA_API_KEY` |
|
|
52
|
+
|
|
53
|
+
Clients with OAuth support can add the same URL as a connector and sign in without putting a key in a config file.
|
|
54
|
+
|
|
55
|
+
<details>
|
|
56
|
+
<summary><b>Claude Code</b></summary>
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
claude mcp add --transport http shopify "https://mcp.hasdata.com/api/mcp?apis=shopify" \
|
|
60
|
+
--header "x-api-key: HASDATA_API_KEY"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
</details>
|
|
64
|
+
|
|
65
|
+
<details>
|
|
66
|
+
<summary><b>Claude Desktop</b></summary>
|
|
67
|
+
|
|
68
|
+
Settings, then Connectors, then Add custom connector, then paste `https://mcp.hasdata.com/api/mcp?apis=shopify` and sign in.
|
|
69
|
+
|
|
70
|
+
For the config-file route, Claude Desktop loads only local (stdio) servers, so it reaches a remote server through a stdio launcher. The `@hasdata/shopify-mcp` package is that launcher, and it reads the key from the environment. Add this to `claude_desktop_config.json`:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"mcpServers": {
|
|
75
|
+
"shopify": {
|
|
76
|
+
"command": "npx",
|
|
77
|
+
"args": ["-y", "@hasdata/shopify-mcp"],
|
|
78
|
+
"env": { "HASDATA_API_KEY": "YOUR_KEY" }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
For Python instead of Node, swap the launcher for the PyPI package, which `uvx` runs without a manual install:
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"mcpServers": {
|
|
89
|
+
"shopify": {
|
|
90
|
+
"command": "uvx",
|
|
91
|
+
"args": ["hasdata-shopify-mcp"],
|
|
92
|
+
"env": { "HASDATA_API_KEY": "YOUR_KEY" }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
</details>
|
|
99
|
+
|
|
100
|
+
<details>
|
|
101
|
+
<summary><b>Cursor</b></summary>
|
|
102
|
+
|
|
103
|
+
`~/.cursor/mcp.json` for every project, or `.cursor/mcp.json` for one:
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"mcpServers": {
|
|
108
|
+
"shopify": {
|
|
109
|
+
"url": "https://mcp.hasdata.com/api/mcp?apis=shopify",
|
|
110
|
+
"headers": { "x-api-key": "HASDATA_API_KEY" }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
</details>
|
|
117
|
+
|
|
118
|
+
<details>
|
|
119
|
+
<summary><b>Windsurf</b></summary>
|
|
120
|
+
|
|
121
|
+
`~/.codeium/windsurf/mcp_config.json`. Windsurf calls the field `serverUrl`, not `url`:
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"mcpServers": {
|
|
126
|
+
"shopify": {
|
|
127
|
+
"serverUrl": "https://mcp.hasdata.com/api/mcp?apis=shopify",
|
|
128
|
+
"headers": { "x-api-key": "HASDATA_API_KEY" }
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
</details>
|
|
135
|
+
|
|
136
|
+
<details>
|
|
137
|
+
<summary><b>VS Code</b></summary>
|
|
138
|
+
|
|
139
|
+
`.vscode/mcp.json` in the workspace:
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
{
|
|
143
|
+
"servers": {
|
|
144
|
+
"shopify": {
|
|
145
|
+
"type": "http",
|
|
146
|
+
"url": "https://mcp.hasdata.com/api/mcp?apis=shopify",
|
|
147
|
+
"headers": { "x-api-key": "HASDATA_API_KEY" }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
</details>
|
|
154
|
+
|
|
155
|
+
## Example prompts
|
|
156
|
+
|
|
157
|
+
Each of these lands on one tool, or on two in sequence when the second needs a handle the first returns.
|
|
158
|
+
|
|
159
|
+
- List the collections on allbirds.com and tell me which ones hold the most products.
|
|
160
|
+
- Pull the first 250 products from this store and group them by `product_type`.
|
|
161
|
+
- Which variants in this store are out of stock right now?
|
|
162
|
+
- Find every product on this store that is discounted, comparing `price` against `compare_at_price`.
|
|
163
|
+
- Page through the shoes collection on this storefront and give me the price range per size.
|
|
164
|
+
- Compare the sock prices on these two Shopify stores.
|
|
165
|
+
|
|
166
|
+
A prompt that names a category rather than a handle takes two calls, one to list the collections and one to pull the products in the matching handle. The collection tool returns `handle`, and that value goes straight into the `collection` argument of the product tool.
|
|
167
|
+
|
|
168
|
+
## Tools
|
|
169
|
+
|
|
170
|
+
Two tools, 5 credits per successful call. Both take a storefront URL and page through the results with `limit` and `page`, where `limit` accepts up to 250.
|
|
171
|
+
|
|
172
|
+
### Get Shopify store products
|
|
173
|
+
|
|
174
|
+
[`hasdata_shopify_products_getProducts`](https://docs.hasdata.com/apis/shopify/products?utm_source=github&utm_medium=syndication&utm_campaign=shopify-mcp)
|
|
175
|
+
|
|
176
|
+
A page of products from one storefront.
|
|
177
|
+
|
|
178
|
+
| Parameter | Type | Required | Notes |
|
|
179
|
+
| :--- | :--- | :--- | :--- |
|
|
180
|
+
| `url` | string | yes | The storefront, such as `https://www.allbirds.com` |
|
|
181
|
+
| `limit` | number | | Products per page, 1 to 250 |
|
|
182
|
+
| `page` | number | | Page number, starting at 1 |
|
|
183
|
+
| `collection` | string | | Restrict to one collection, by its handle |
|
|
184
|
+
|
|
185
|
+
Returns a `products` array. Each product carries `id`, `title`, `handle`, `body_html`, `vendor`, `product_type`, `tags`, `published_at`, `created_at`, `updated_at`, an `options` array naming the axes the variants vary on, an `images` array, and a `variants` array.
|
|
186
|
+
|
|
187
|
+
The price lives on the variant, never on the product. A variant carries `id`, `title`, `sku`, `price`, `compare_at_price`, `available`, `grams`, `position`, `requires_shipping`, `taxable`, the `option1` through `option3` values and its own timestamps.
|
|
188
|
+
|
|
189
|
+
```json
|
|
190
|
+
{
|
|
191
|
+
"id": 6889962537040,
|
|
192
|
+
"title": "Anytime Ankle Sock - Basin Blue",
|
|
193
|
+
"handle": "anytime-ankle-sock-basin-blue",
|
|
194
|
+
"vendor": "Allbirds",
|
|
195
|
+
"product_type": "Socks",
|
|
196
|
+
"updated_at": "2026-09-09T05:25:09-07:00",
|
|
197
|
+
"options": [{ "name": "Size", "position": 1, "values": ["S (W5-7)", "M (W8-10 / M8)", "L (W11 / M9-12)", "XL (M13-14)"] }],
|
|
198
|
+
"variants": [
|
|
199
|
+
{
|
|
200
|
+
"id": 40356485202000,
|
|
201
|
+
"title": "S (W5-7)",
|
|
202
|
+
"sku": "A10842U001",
|
|
203
|
+
"price": "16.00",
|
|
204
|
+
"compare_at_price": null,
|
|
205
|
+
"available": false,
|
|
206
|
+
"grams": 59,
|
|
207
|
+
"position": 1
|
|
208
|
+
}
|
|
209
|
+
],
|
|
210
|
+
"images": [
|
|
211
|
+
{
|
|
212
|
+
"id": 36355227844688,
|
|
213
|
+
"position": 1,
|
|
214
|
+
"src": "https://cdn.shopify.com/s/files/1/1104/4168/files/A10842_S24Q1_Anytime_Ankle_Sock_Basin_Blue_A-1400x1400.png?v=1776183348",
|
|
215
|
+
"width": 1400,
|
|
216
|
+
"height": 1400
|
|
217
|
+
}
|
|
218
|
+
]
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Get Shopify store collections
|
|
223
|
+
|
|
224
|
+
[`hasdata_shopify_collections_getCollections`](https://docs.hasdata.com/apis/shopify/collections?utm_source=github&utm_medium=syndication&utm_campaign=shopify-mcp)
|
|
225
|
+
|
|
226
|
+
The collections that organise one storefront.
|
|
227
|
+
|
|
228
|
+
| Parameter | Type | Required | Notes |
|
|
229
|
+
| :--- | :--- | :--- | :--- |
|
|
230
|
+
| `url` | string | yes | The storefront, such as `https://www.allbirds.com` |
|
|
231
|
+
| `limit` | number | | Collections per page, 1 to 250 |
|
|
232
|
+
| `page` | number | | Page number, starting at 1 |
|
|
233
|
+
|
|
234
|
+
Returns a `collections` array. Each entry carries `id`, `title`, `handle`, `description`, `image`, `products_count`, `published_at` and `updated_at`.
|
|
235
|
+
|
|
236
|
+
This is the merchandising taxonomy as the store publishes it, which makes it the cheapest way to see how a competitor groups a catalogue before pulling any products. The `handle` is the join key into the product tool.
|
|
237
|
+
|
|
238
|
+
```json
|
|
239
|
+
{
|
|
240
|
+
"id": 135995326544,
|
|
241
|
+
"title": "Accessories",
|
|
242
|
+
"handle": "womens-accessories",
|
|
243
|
+
"description": "You know what they say: It's all in the details. Customize your look with planet-friendly face masks, hats, and more. ",
|
|
244
|
+
"published_at": "2019-08-05T14:01:17-07:00",
|
|
245
|
+
"updated_at": "2026-07-08T13:39:17-07:00",
|
|
246
|
+
"image": null,
|
|
247
|
+
"products_count": 28
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Errors and failure paths
|
|
252
|
+
|
|
253
|
+
Plan for these rather than assuming a happy path.
|
|
254
|
+
|
|
255
|
+
**`price` and `compare_at_price` are strings, not numbers.** They arrive as `"16.00"`, exactly as the storefront publishes them. Convert before you compare or sum, because string ordering puts `"9.00"` above `"16.00"`.
|
|
256
|
+
|
|
257
|
+
**A product has no price of its own.** Anything about cost has to go through the `variants` array, and a product with a size or colour axis usually has several prices. Reading the first variant and calling it the price is the most common mistake here.
|
|
258
|
+
|
|
259
|
+
**`compare_at_price` is null when nothing is discounted**, so a discount check is a null test first and a comparison second.
|
|
260
|
+
|
|
261
|
+
**`available` is per variant and reflects the moment of the call.** A product is not out of stock, a variant is, and stock moves. Two calls minutes apart can disagree, which is the point when you are monitoring, and a trap when you are diffing catalogues.
|
|
262
|
+
|
|
263
|
+
**A collection can report `products_count` of zero.** Stores leave empty, staged and seasonal collections published, so an empty collection is normal rather than a failed call.
|
|
264
|
+
|
|
265
|
+
**Stores publish things that are not for sale.** Internal, retired and staging items sit in the public catalogue on plenty of stores, sometimes flagged in the title and sometimes not. Filter on what you actually need instead of trusting that every row is a live product.
|
|
266
|
+
|
|
267
|
+
**`tags` are whatever the merchant wrote.** Some stores use them as plain keywords, others push namespaced metafield strings into them. Treat the array as free text.
|
|
268
|
+
|
|
269
|
+
**`body_html` is HTML.** Strip it before you index or embed the description.
|
|
270
|
+
|
|
271
|
+
**`collection.image` is often null**, and so is `featured_image` on a variant. Fall back to the product `images` array.
|
|
272
|
+
|
|
273
|
+
**A URL that is not a classic Shopify storefront still answers 200 and still bills.** There is no `products` array in that response and an `error` string in its place, while `requestMetadata.status` stays `ok`. Test for the array before you read it, because the shape changes rather than the status.
|
|
274
|
+
|
|
275
|
+
**A headless Shopify store fails on its custom domain and works on its `myshopify.com` one.** Headless shops serve the storefront from their own front end, so the catalogue is not published under the public domain. When a store you know runs Shopify comes back with the `error` string, retry it as `https://<shop>.myshopify.com`.
|
|
276
|
+
|
|
277
|
+
Results that carry data also carry a `requestMetadata.id` worth quoting in support.
|
|
278
|
+
|
|
279
|
+
## Pricing, free tier and limits
|
|
280
|
+
|
|
281
|
+
Each Shopify tool costs **5 credits per successful call**. Response size does not change the price, so a 250-product page and a 3-product page cost the same, which makes the largest page the cheapest way to mirror a catalogue.
|
|
282
|
+
|
|
283
|
+
The free tier is **1,000 credits every month with no card**, which is 200 Shopify calls at the base rate. It renews with the billing cycle, so a low-volume agent runs on the free tier indefinitely.
|
|
284
|
+
|
|
285
|
+
Paid plans start at **$49 a month** for 200,000 credits, which is 40,000 calls. The unit price falls with volume, from **$1.23 per 1,000 calls** on the entry plan to **$0.50** on Business, **$0.42** on Growth and **$0.37** on the largest [high-volume plans](https://hasdata.com/prices?utm_source=github&utm_medium=syndication&utm_campaign=shopify-mcp).
|
|
286
|
+
|
|
287
|
+
Your plan also sets concurrency. The free tier allows 1 request at a time, Startup 15, Business 30, Growth 50, and the high-volume plans run from 200 to 1,500. Handle the overflow case defensively in anything unattended, because an agent that fans out across stores will reach the ceiling before you do.
|
|
288
|
+
|
|
289
|
+
A request that comes back non-200 is not billed. A successful call that finds nothing is still a call.
|
|
290
|
+
|
|
291
|
+
## Tool selection
|
|
292
|
+
|
|
293
|
+
Start from what the prompt gives you. A question about the catalogue itself goes to the product tool. A question about how the store is organised, or a prompt that names a category by its shop-facing name, goes to the collection tool first.
|
|
294
|
+
|
|
295
|
+
Then think about page size. Both tools accept `limit` up to 250 and cost the same at any size, so a catalogue of 900 products is four calls, not ninety. Leaving `limit` at its default is the most expensive habit you can pick up here.
|
|
296
|
+
|
|
297
|
+
Filter server-side when you can. Passing `collection` to the product tool costs one call and returns the subset, where pulling the whole catalogue and filtering locally costs one call per page of everything you did not want.
|
|
298
|
+
|
|
299
|
+
## How it compares
|
|
300
|
+
|
|
301
|
+
Shopify's Admin API is the official route to a store's catalogue, and it answers a different question.
|
|
302
|
+
|
|
303
|
+
| | Shopify Admin API | This server |
|
|
304
|
+
| :--- | :--- | :--- |
|
|
305
|
+
| Which stores | The ones you own or were granted access to | Any public storefront |
|
|
306
|
+
| Setup | Create an app, request scopes, hold a token per store | One header |
|
|
307
|
+
| Credential per store | Yes | No |
|
|
308
|
+
| Inventory levels | Exact counts | An `available` flag per variant |
|
|
309
|
+
| Draft and hidden products | Returned | Not returned, they are not public |
|
|
310
|
+
| Orders and customers | Returned | Not returned |
|
|
311
|
+
| Cost | Free within rate limits | Paid past the free tier, 5 credits a call |
|
|
312
|
+
|
|
313
|
+
The row that decides it is which stores. The Admin API is built for a merchant working on their own shop, and it needs a token that only that merchant can issue, which rules it out for comparing yourself against ten competitors. When the store is yours, the Admin API is more complete and free, and you should use it.
|
|
314
|
+
|
|
315
|
+
## FAQ
|
|
316
|
+
|
|
317
|
+
### Does Shopify have its own MCP server?
|
|
318
|
+
|
|
319
|
+
Yes, and it does something else. Every eligible storefront exposes one at its own domain, and its tools are built for an agent that is shopping, such as searching the catalogue, building a cart and running a checkout. It is per-store, so an agent comparing thirty shops needs thirty connections. This server is for reading catalogues in bulk across arbitrary stores, so the two do not overlap. If your agent is buying from one shop, use Shopify's.
|
|
320
|
+
|
|
321
|
+
### What is a Shopify MCP server?
|
|
322
|
+
|
|
323
|
+
An MCP server exposes tools an AI client can call. This one turns the public catalogue of any Shopify storefront into JSON an agent can reason over, without a browser or a scraping library in your stack.
|
|
324
|
+
|
|
325
|
+
### Do I need a Shopify account, an app or a merchant token?
|
|
326
|
+
|
|
327
|
+
No. The only credential is your HasData key.
|
|
328
|
+
|
|
329
|
+
### Does it work on custom domains?
|
|
330
|
+
|
|
331
|
+
For a classic storefront, yes, and a store on its own domain is the same as one on `myshopify.com`. A headless store is the exception. Its front end is served by something other than Shopify, so the catalogue is not published under the custom domain and the call comes back empty. Retry those as `https://<shop>.myshopify.com`.
|
|
332
|
+
|
|
333
|
+
### How do I tell whether a site runs Shopify?
|
|
334
|
+
|
|
335
|
+
Call the product tool on it, then look at the shape rather than the status. A classic Shopify storefront answers with a `products` array. Anything else answers 200 with an `error` string and no array, and that response is billed like any other successful call.
|
|
336
|
+
|
|
337
|
+
### Can I get inventory counts?
|
|
338
|
+
|
|
339
|
+
No, only the `available` flag each variant publishes. Exact stock levels are not public, and they come from the Admin API on a store you control.
|
|
340
|
+
|
|
341
|
+
### How do I pull a whole catalogue?
|
|
342
|
+
|
|
343
|
+
Page with `limit` at 250 and step `page` until a page comes back short or empty. Cost scales with pages, not with products, so the largest page size is always the cheapest route.
|
|
344
|
+
|
|
345
|
+
### Can I use this together with other HasData APIs?
|
|
346
|
+
|
|
347
|
+
Yes. One key covers everything, and one endpoint serves them all through the `apis` parameter. Point a client at `?apis=shopify,amazon` to get both tool sets in one connection, or at [`mcp.hasdata.com/api/mcp`](https://docs.hasdata.com/mcp-server?utm_source=github&utm_medium=syndication&utm_campaign=shopify-mcp) for the full catalogue.
|
|
348
|
+
|
|
349
|
+
### Is HasData affiliated with Shopify?
|
|
350
|
+
|
|
351
|
+
No. HasData is an independent service and is not affiliated with, endorsed by, or sponsored by Shopify. Shopify is a trademark of its respective owner. The tools work with publicly available data only, and you are responsible for using the results in line with the terms of the stores you read and the law that applies to you.
|
|
352
|
+
|
|
353
|
+
### Compliance and personal data
|
|
354
|
+
|
|
355
|
+
A product catalogue is business data, and these tools return no customer, order or contact information. The `vendor` field can carry a sole trader's own name on a small store, which is the one place a person can appear. Storing a competitor's catalogue is a commercial decision rather than a privacy one, so read the terms of the store you are pulling from and check your own obligations.
|
|
356
|
+
|
|
357
|
+
## HasData links
|
|
358
|
+
|
|
359
|
+
- [Shopify Scraper API](https://hasdata.com/apis/shopify-api?utm_source=github&utm_medium=syndication&utm_campaign=shopify-mcp), the REST endpoints behind these tools
|
|
360
|
+
- [API documentation](https://docs.hasdata.com/apis/shopify/products?utm_source=github&utm_medium=syndication&utm_campaign=shopify-mcp)
|
|
361
|
+
- [MCP server documentation](https://docs.hasdata.com/mcp-server?utm_source=github&utm_medium=syndication&utm_campaign=shopify-mcp)
|
|
362
|
+
- [Pricing](https://hasdata.com/prices?utm_source=github&utm_medium=syndication&utm_campaign=shopify-mcp)
|
|
363
|
+
- [Dashboard](https://app.hasdata.com/sign-up?utm_source=github&utm_medium=syndication&utm_campaign=shopify-mcp)
|
|
364
|
+
|
|
365
|
+
Other HasData MCP servers: [Google Search](https://github.com/HasData/google-search-mcp), [Google Maps](https://github.com/HasData/google-maps-mcp), [Google Trends](https://github.com/HasData/google-trends-mcp), [Google Flights](https://github.com/HasData/google-flights-mcp), [DuckDuckGo](https://github.com/HasData/duckduckgo-mcp), [YouTube](https://github.com/HasData/youtube-mcp), [TikTok](https://github.com/HasData/tiktok-mcp), [Instagram](https://github.com/HasData/instagram-mcp), [Amazon](https://github.com/HasData/amazon-mcp), [Yelp](https://github.com/HasData/yelp-mcp), [Zillow](https://github.com/HasData/zillow-mcp), [Airbnb](https://github.com/HasData/airbnb-mcp), [Booking.com](https://github.com/HasData/booking-mcp), [Indeed](https://github.com/HasData/indeed-mcp).
|
|
366
|
+
|
|
367
|
+
## Development
|
|
368
|
+
|
|
369
|
+
The launcher is a thin stdio bridge to the remote server, so there is nothing to build.
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
npm install
|
|
373
|
+
HASDATA_API_KEY=your_key_here npm test
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
The tests in `test/` assert the tool contract, the part that can break without a commit here. They check that `?apis=shopify` returns the expected tool count, that no name changed, that every tool still declares its required parameter and carries a description, and that the key in use is actually accepted. That last check calls a tool for real and costs 5 credits, which is the price of a canary that can fail for the right reason.
|
|
377
|
+
|
|
378
|
+
The contract suite also runs weekly on a schedule, because the upstream tool list can change without anyone touching this repository.
|
|
379
|
+
|
|
380
|
+
## Contributing
|
|
381
|
+
|
|
382
|
+
A tool table, a response sample or a documented behaviour that does not match reality is worth an issue. There is a template for exactly that. Pull requests are welcome for the same, and for anything in the launcher.
|
|
383
|
+
|
|
384
|
+
## License
|
|
385
|
+
|
|
386
|
+
MIT, see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Thin launcher for HasData's hosted Shopify MCP server.
|
|
2
|
+
|
|
3
|
+
Connects an MCP client to the remote streamable-HTTP endpoint through mcp-proxy.
|
|
4
|
+
The server runs on HasData's infrastructure. This package only proxies stdio to it.
|
|
5
|
+
"""
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import subprocess
|
|
9
|
+
|
|
10
|
+
URL = "https://mcp.hasdata.com/api/mcp?apis=shopify"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main() -> None:
|
|
14
|
+
key = os.environ.get("HASDATA_API_KEY")
|
|
15
|
+
if not key:
|
|
16
|
+
sys.stderr.write(
|
|
17
|
+
"HASDATA_API_KEY is not set. Create a free key at https://app.hasdata.com "
|
|
18
|
+
"and set HASDATA_API_KEY.\n"
|
|
19
|
+
)
|
|
20
|
+
raise SystemExit(1)
|
|
21
|
+
args = [
|
|
22
|
+
sys.executable, "-m", "mcp_proxy", URL,
|
|
23
|
+
"--transport=streamablehttp",
|
|
24
|
+
"--headers", "x-api-key", key,
|
|
25
|
+
]
|
|
26
|
+
rc = subprocess.call(args)
|
|
27
|
+
# subprocess returns -N when mcp_proxy is killed by signal N; map to 128+N.
|
|
28
|
+
raise SystemExit(rc if rc >= 0 else 128 - rc)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Thin launcher: connects an MCP client to HasData's hosted Shopify MCP server
|
|
3
|
+
// (streamable HTTP) through the mcp-remote stdio bridge. The server runs remotely.
|
|
4
|
+
// This package only proxies, so nothing here scrapes anything.
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
|
|
9
|
+
const URL = 'https://mcp.hasdata.com/api/mcp?apis=shopify';
|
|
10
|
+
const key = process.env.HASDATA_API_KEY;
|
|
11
|
+
if (!key) {
|
|
12
|
+
process.stderr.write('HASDATA_API_KEY is not set. Create a free key at https://app.hasdata.com and set HASDATA_API_KEY.\n');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
// Resolve mcp-remote's CLI from its own package.json bin, so a future layout change
|
|
16
|
+
// or an exports map does not break a hardcoded deep path.
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
const pkg = require('mcp-remote/package.json');
|
|
19
|
+
const proxy = join(dirname(require.resolve('mcp-remote/package.json')), pkg.bin['mcp-remote']);
|
|
20
|
+
const child = spawn(process.execPath, [proxy, URL, '--header', `x-api-key:${key}`], { stdio: 'inherit' });
|
|
21
|
+
child.on('exit', (code, signal) => {
|
|
22
|
+
if (signal) process.kill(process.pid, signal);
|
|
23
|
+
else process.exit(code ?? 0);
|
|
24
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hasdata/shopify-mcp",
|
|
3
|
+
"mcpName": "com.hasdata/shopify",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "MCP server for Shopify storefronts through HasData's hosted API: pull the public product catalogue and collections from any store by URL, with variants, SKUs and prices. No app, no merchant token. 1,000 free credits every month.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"hasdata-shopify-mcp": "index.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.mjs",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "node --test"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"mcp-remote": "^0.1.43"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/HasData/shopify-mcp.git"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://hasdata.com/apis/shopify-api",
|
|
30
|
+
"keywords": [
|
|
31
|
+
"mcp",
|
|
32
|
+
"shopify",
|
|
33
|
+
"shopify-mcp",
|
|
34
|
+
"ecommerce",
|
|
35
|
+
"product-data",
|
|
36
|
+
"price-monitoring",
|
|
37
|
+
"model-context-protocol",
|
|
38
|
+
"hasdata",
|
|
39
|
+
"scraper"
|
|
40
|
+
]
|
|
41
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "hasdata-shopify-mcp"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "MCP server for Shopify through HasData's hosted API. 1,000 free credits every month."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
keywords = ["mcp", "shopify", "ecommerce", "product-data", "price-monitoring", "model-context-protocol", "hasdata"]
|
|
9
|
+
# mcp is pinned below 2 because mcp-proxy 0.12.0 imports request_ctx, removed in the mcp 2.x SDK.
|
|
10
|
+
dependencies = ["mcp-proxy>=0.12.0", "mcp>=1.17,<2"]
|
|
11
|
+
|
|
12
|
+
[project.urls]
|
|
13
|
+
Homepage = "https://hasdata.com/apis/shopify-api"
|
|
14
|
+
Repository = "https://github.com/HasData/shopify-mcp"
|
|
15
|
+
|
|
16
|
+
[project.scripts]
|
|
17
|
+
hasdata-shopify-mcp = "hasdata_shopify_mcp:main"
|
|
18
|
+
|
|
19
|
+
[build-system]
|
|
20
|
+
requires = ["hatchling"]
|
|
21
|
+
build-backend = "hatchling.build"
|
|
22
|
+
|
|
23
|
+
[tool.hatch.build.targets.wheel]
|
|
24
|
+
packages = ["hasdata_shopify_mcp"]
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "com.hasdata/shopify",
|
|
4
|
+
"title": "HasData Shopify",
|
|
5
|
+
"description": "The public product catalogue and collections of any Shopify storefront, as structured JSON.",
|
|
6
|
+
"version": "1.0.0",
|
|
7
|
+
"websiteUrl": "https://hasdata.com/apis/shopify-api",
|
|
8
|
+
"repository": {
|
|
9
|
+
"url": "https://github.com/HasData/shopify-mcp",
|
|
10
|
+
"source": "github"
|
|
11
|
+
},
|
|
12
|
+
"remotes": [
|
|
13
|
+
{
|
|
14
|
+
"type": "streamable-http",
|
|
15
|
+
"url": "https://mcp.hasdata.com/api/mcp?apis=shopify",
|
|
16
|
+
"headers": [
|
|
17
|
+
{
|
|
18
|
+
"name": "x-api-key",
|
|
19
|
+
"description": "HasData API key, created at app.hasdata.com. 1,000 free credits every month, no card.",
|
|
20
|
+
"isRequired": true,
|
|
21
|
+
"isSecret": true
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"packages": [
|
|
27
|
+
{
|
|
28
|
+
"registryType": "npm",
|
|
29
|
+
"identifier": "@hasdata/shopify-mcp",
|
|
30
|
+
"version": "1.0.0",
|
|
31
|
+
"transport": {
|
|
32
|
+
"type": "stdio"
|
|
33
|
+
},
|
|
34
|
+
"environmentVariables": [
|
|
35
|
+
{
|
|
36
|
+
"name": "HASDATA_API_KEY",
|
|
37
|
+
"description": "HasData API key, created at app.hasdata.com. 1,000 free credits every month, no card.",
|
|
38
|
+
"isRequired": true,
|
|
39
|
+
"isSecret": true
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"registryType": "pypi",
|
|
45
|
+
"identifier": "hasdata-shopify-mcp",
|
|
46
|
+
"version": "1.0.0",
|
|
47
|
+
"runtimeHint": "uvx",
|
|
48
|
+
"transport": {
|
|
49
|
+
"type": "stdio"
|
|
50
|
+
},
|
|
51
|
+
"environmentVariables": [
|
|
52
|
+
{
|
|
53
|
+
"name": "HASDATA_API_KEY",
|
|
54
|
+
"description": "HasData API key, created at app.hasdata.com. 1,000 free credits every month, no card.",
|
|
55
|
+
"isRequired": true,
|
|
56
|
+
"isSecret": true
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// Tool contract test.
|
|
2
|
+
//
|
|
3
|
+
// The README promises two tools with specific names and required parameters. The upstream list
|
|
4
|
+
// can change without a single commit here, and the README would start lying silently. These
|
|
5
|
+
// checks catch that before a user does.
|
|
6
|
+
//
|
|
7
|
+
// One test calls a tool for real. Listing tools accepts any non-empty key, so a contract check
|
|
8
|
+
// that only lists tools stays green with a revoked or mistyped key. That call costs 5 credits,
|
|
9
|
+
// which is the price of a canary that can fail for the right reason.
|
|
10
|
+
//
|
|
11
|
+
// Run: HASDATA_API_KEY=your_key_here npm test
|
|
12
|
+
|
|
13
|
+
import { test } from 'node:test';
|
|
14
|
+
import assert from 'node:assert/strict';
|
|
15
|
+
|
|
16
|
+
const ENDPOINT = 'https://mcp.hasdata.com/api/mcp?apis=shopify';
|
|
17
|
+
const KEY = process.env.HASDATA_API_KEY;
|
|
18
|
+
const TIMEOUT_MS = 30_000;
|
|
19
|
+
|
|
20
|
+
const EXPECTED = {
|
|
21
|
+
hasdata_shopify_products_getProducts: ['url'],
|
|
22
|
+
hasdata_shopify_collections_getCollections: ['url'],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// A streamable HTTP body arrives either as plain JSON or as server-sent events. One SSE event
|
|
26
|
+
// can span several data: lines, several events can share one response, and a server is free to
|
|
27
|
+
// send progress notifications before the answer. So collect every event and pick the message
|
|
28
|
+
// carrying our request id instead of trusting the first data: line.
|
|
29
|
+
function parseRpc(raw, id) {
|
|
30
|
+
const trimmed = raw.trim();
|
|
31
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('[')) return JSON.parse(trimmed);
|
|
32
|
+
|
|
33
|
+
const messages = [];
|
|
34
|
+
for (const event of trimmed.split(/\r?\n\r?\n+/)) {
|
|
35
|
+
const data = event
|
|
36
|
+
.split(/\r?\n/)
|
|
37
|
+
.filter((l) => l.startsWith('data:'))
|
|
38
|
+
.map((l) => l.slice(5).replace(/^ /, ''))
|
|
39
|
+
.join('\n');
|
|
40
|
+
if (!data || data === '[DONE]') continue;
|
|
41
|
+
try {
|
|
42
|
+
messages.push(JSON.parse(data));
|
|
43
|
+
} catch {
|
|
44
|
+
// A keep-alive or a partial event is not our response.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
assert.ok(messages.length, `no JSON-RPC message in the response: ${raw.slice(0, 300)}`);
|
|
48
|
+
const match = messages.find((m) => m.id === id);
|
|
49
|
+
assert.ok(match, `no message with id ${id} in the response: ${raw.slice(0, 300)}`);
|
|
50
|
+
return match;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let nextId = 1;
|
|
54
|
+
|
|
55
|
+
async function rpc(method, params = {}) {
|
|
56
|
+
// The CI key sits on the free plan, where concurrency is 1. When several of
|
|
57
|
+
// these repos are pushed at once their contract runs collide, and HasData
|
|
58
|
+
// answers 429 with code concurrency_limit straight away rather than queueing.
|
|
59
|
+
// That is a plan limit, not a broken contract, so the call is retried before
|
|
60
|
+
// the test gives up. A 401 still fails on the first attempt.
|
|
61
|
+
for (let attempt = 1; ; attempt++) {
|
|
62
|
+
const id = nextId++;
|
|
63
|
+
const res = await fetch(ENDPOINT, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: {
|
|
66
|
+
'x-api-key': KEY,
|
|
67
|
+
'Content-Type': 'application/json',
|
|
68
|
+
// The server answers over streamable HTTP, so accept both a plain body and a stream.
|
|
69
|
+
Accept: 'application/json, text/event-stream',
|
|
70
|
+
},
|
|
71
|
+
body: JSON.stringify({ jsonrpc: '2.0', id, method, params }),
|
|
72
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
73
|
+
});
|
|
74
|
+
assert.equal(res.status, 200, `${method} returned ${res.status}`);
|
|
75
|
+
const raw = await res.text();
|
|
76
|
+
if (raw.includes('concurrency_limit') && attempt < 5) {
|
|
77
|
+
await new Promise((r) => setTimeout(r, attempt * 4000));
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
return { raw, body: parseRpc(raw, id) };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// One network round trip for every test that needs the list.
|
|
85
|
+
let toolsPromise;
|
|
86
|
+
function listTools() {
|
|
87
|
+
toolsPromise ??= rpc('tools/list').then(({ body }) => {
|
|
88
|
+
assert.ok(body.result?.tools, 'the response carried no result.tools');
|
|
89
|
+
return body.result.tools;
|
|
90
|
+
});
|
|
91
|
+
return toolsPromise;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const live = { skip: KEY ? false : 'HASDATA_API_KEY is not set, skipping the live checks' };
|
|
95
|
+
|
|
96
|
+
test('apis=shopify exposes the documented tools and nothing else', live, async () => {
|
|
97
|
+
const tools = await listTools();
|
|
98
|
+
const names = tools.map((t) => t.name).sort().join(', ');
|
|
99
|
+
assert.equal(
|
|
100
|
+
tools.length,
|
|
101
|
+
Object.keys(EXPECTED).length,
|
|
102
|
+
`expected ${Object.keys(EXPECTED).length} tools, got ${tools.length}: ${names}`
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('the tool names have not changed', live, async () => {
|
|
107
|
+
const tools = await listTools();
|
|
108
|
+
const names = new Set(tools.map((t) => t.name));
|
|
109
|
+
for (const expected of Object.keys(EXPECTED)) {
|
|
110
|
+
assert.ok(names.has(expected), `tool ${expected} is missing from the list`);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('every tool still declares its required parameter', live, async () => {
|
|
115
|
+
const tools = await listTools();
|
|
116
|
+
for (const tool of tools) {
|
|
117
|
+
const required = tool.inputSchema?.required ?? [];
|
|
118
|
+
const want = EXPECTED[tool.name];
|
|
119
|
+
assert.ok(want, `tool ${tool.name} is not covered by this test`);
|
|
120
|
+
for (const param of want) {
|
|
121
|
+
assert.ok(
|
|
122
|
+
required.includes(param),
|
|
123
|
+
`${tool.name} should require ${param}, declares: ${required.join(', ') || 'nothing'}`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('every tool carries a description', live, async () => {
|
|
130
|
+
const tools = await listTools();
|
|
131
|
+
for (const tool of tools) {
|
|
132
|
+
assert.ok(
|
|
133
|
+
(tool.description || '').trim().length > 20,
|
|
134
|
+
`${tool.name} has an empty or near-empty description`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// The README tells readers to list collections, then pass a handle back as `collection` to get
|
|
140
|
+
// that subset in one call. That workflow is the cheapest route through a large catalogue, and it
|
|
141
|
+
// only exists while the parameter does.
|
|
142
|
+
test('the product tool still accepts a collection handle', live, async () => {
|
|
143
|
+
const tools = await listTools();
|
|
144
|
+
const products = tools.find((t) => t.name === 'hasdata_shopify_products_getProducts');
|
|
145
|
+
assert.ok(products, 'the product tool is missing from the list');
|
|
146
|
+
const props = products.inputSchema?.properties ?? {};
|
|
147
|
+
for (const param of ['collection', 'limit', 'page']) {
|
|
148
|
+
assert.ok(props[param], `the product tool no longer accepts ${param}`);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('the key is accepted by HasData', live, async () => {
|
|
153
|
+
const { raw } = await rpc('tools/call', {
|
|
154
|
+
name: 'hasdata_shopify_products_getProducts',
|
|
155
|
+
arguments: { url: 'https://www.allbirds.com', limit: 1 },
|
|
156
|
+
});
|
|
157
|
+
assert.ok(!raw.includes('401 Unauthorized'), 'HasData rejected the key');
|
|
158
|
+
assert.ok(!raw.includes('"isError":true'), `the tool call failed: ${raw.slice(0, 300)}`);
|
|
159
|
+
});
|