Shopify Enterprise MCP Server
Production-grade MCP server that exposes live Shopify Admin GraphQL data to Microsoft Copilot Studio agents, enabling real-time answers to business questions about inventory, orders, and products.
README
Shopify Enterprise MCP Server
Production-grade Model Context Protocol server that exposes live Shopify Admin GraphQL data to Microsoft Copilot Studio agents.
Every business question is answered by querying Shopify at request time. The server never returns cached figures and instructs the connected agent never to answer from prior knowledge — inventory, orders and sales change continuously, so a remembered number is a wrong number.
Status
Complete. Twenty-one tools — seventeen read-only, four that change the store — implemented, unit tested and verified against a live Shopify store.
| Area | State |
|---|---|
| Build, TypeScript strict mode, ESLint, Prettier | ✅ Complete |
| Configuration + fail-fast environment validation | ✅ Complete |
| Shopify authentication (client credentials + renewal) | ✅ Complete |
| Error hierarchy + RFC 9457 global handler | ✅ Complete |
| Structured logging + correlation context | ✅ Complete |
| Application Insights telemetry | ✅ Complete |
| Shopify GraphQL client (retry, cost, throttle, paging) | ✅ Complete |
| MCP registry, server factory, Streamable HTTP transport | ✅ Complete |
| Health / readiness / version endpoints | ✅ Complete |
| All 21 MCP tools | ✅ Complete |
| Store-changing tools, gated and confirmed | ✅ Complete |
| Test suite | ✅ 446 passing |
Available MCP tools
Catalogue
| Tool | Answers |
|---|---|
search_products |
"How many Nike shoes are in stock?", "Find products under 50", "What perfumes do we sell?" |
get_product_details |
Full record for one product: every variant, price, SKU, stock level, options and collections. |
recommend_related_products |
"What else would go with this?" — catalogue similarity, not purchase behaviour. |
get_collections |
"What collections do we have?", "Is that collection rule-based or curated?" |
Orders, fulfilment and customers
| Tool | Answers |
|---|---|
get_orders |
"How many orders this week?", "Show unfulfilled orders", "Any refunds?" |
get_order_by_id |
Everything about one order: line items, addresses, fulfilment, refunds. |
get_fulfillments |
"What has shipped?", "How long are we taking to despatch?", "No tracking?" |
get_refunds |
"How much have we refunded?", "What came back, and was it restocked?" |
search_customers |
"Who are our customers?", "Find customers tagged VIP", "Customers in India" |
get_customer |
One customer's full profile, addresses and lifetime value. |
get_customer_purchase_history |
"What has this person bought?", "When did they last order?" |
Inventory
| Tool | Answers |
|---|---|
get_inventory |
"What's in stock?", "Stock level for SKU X", "Anything oversold?" |
get_low_stock_products |
"What's running low?", "What do we need to reorder?" |
Commercial
| Tool | Answers |
|---|---|
get_sales_summary |
"How much did we sell last month?", "What's our average order value?" |
get_best_selling_products |
"What are our top sellers?", "Which product made the most revenue?" |
get_discounts |
"What promotions are running?", "Is the free shipping offer still active?" |
get_abandoned_carts |
"How many abandoned carts?", "What revenue are we losing at checkout?" |
Every list tool filters and paginates server-side in Shopify. A price filter applied locally could only narrow the page that happened to be fetched, so "products under 50" would silently come to mean "products under 50 among the 20 that came back".
Tools that change the store
| Tool | Effect |
|---|---|
create_product |
Adds a product. Created as DRAFT unless ACTIVE is asked for. |
update_product |
Changes title, description, vendor, type, tags or status. |
update_inventory_levels |
Sets or adjusts stock on hand, which decides what customers can buy. |
create_refund |
Returns real money to a customer. Cannot be undone. |
Three independent controls stand in front of them:
- Registration gate.
MCP_ENABLE_WRITE_TOOLS=falsewithholds all four fromtools/listentirely — an agent cannot call what was never published. It is a single App Service setting, so the capability can be withdrawn during an incident without a redeploy. - Explicit confirmation. Every one requires
confirm: true. Called without it they change nothing and instead report exactly what would change — for stock, after reading the live level, so the user approves real before-and-after figures rather than an intention. userErrorsenforcement. Shopify rejects a mutation with HTTP 200, a null payload and the reason inuserErrors. Every mutation response is checked, so a write that did not happen is never reported as one that did.
create_refund adds two more: the amount is always computed by Shopify's own suggestedRefund
rather than by this server, and MCP_MAX_REFUND_AMOUNT can cap any single refund.
Every completed change writes an audit line carrying the correlation ID; refunds log at warn.
Documentation
| Document | Covers |
|---|---|
| architecture.md | Layering, request flow, and the reasoning behind each design decision |
| configuration.md | Every environment variable, production invariants, common misconfigurations |
| deployment.md | Full Azure App Service procedure, slots, scaling, rollback, checklist |
| copilot-studio.md | Connector import, agent wiring, grounding instructions, troubleshooting |
| copilot-studio-connector.yaml | Power Platform custom connector definition |
| tools.md | Each tool with real request/response examples |
| operations.md | Runbook: KQL queries, alerts, incident diagnosis, rotation |
Architecture
┌──────────────────────┐ Streamable HTTP (JSON-RPC 2.0) ┌────────────────────────────┐
│ Copilot Studio │ ─────────────────────────────────► │ Azure App Service (Linux) │
│ custom connector │ HTTPS + x-api-key │ Fastify + MCP SDK │
└──────────────────────┘ └─────────────┬──────────────┘
│
┌─────────────────────────────────────────────────┘
▼
┌───────────────────────────┐ Correlation context (AsyncLocalStorage)
│ MCP transport │ flows through every layer below
│ (stateless per request) │
└─────────────┬─────────────┘
▼
┌───────────────────────────┐ Cross-cutting: validation, timing,
│ Tool executor │ envelope, error → result, telemetry
└─────────────┬─────────────┘
▼
┌───────────────────────────┐ Business logic only
│ Service layer │
└─────────────┬─────────────┘
▼
┌───────────────────────────┐ Retry · cost governor · pagination
│ ShopifyGraphQLClient │ error mapping · dependency telemetry
└─────────────┬─────────────┘
▼
Shopify Admin GraphQL API
Layer rules
- Tools contain no business logic. They validate input, call a service, and return a summary plus structured data.
- Services contain no MCP or HTTP concepts. They accept typed arguments and return domain objects.
- The Shopify client is the only egress point. Nothing else issues an outbound request.
- Configuration is read once, validated, frozen. No module touches
process.envdirectly.
Project structure
src/
app/ Fastify composition root, plugins, routes, error handler
config/ Environment schema, validated + frozen configuration
mcp/
registry/ Tool definitions and the process-wide tool catalogue
server/ McpServer factory and the tool execution wrapper
transport/ Streamable HTTP transport management
tools/ MCP tool definitions (composition root)
services/ Domain services (product, order, inventory, customer, …)
shopify/
graphql-client.ts Retry, cost governance, error mapping, telemetry
cost-governor.ts Client-side model of Shopify's leaky bucket
error-mapper.ts HTTP + GraphQL failure classification
pagination.ts Bounded cursor pagination
queries/ Reusable GraphQL documents
types/ Connection, money and cost types
middleware/
auth/ API key authentication
logging/ Pino logger and AsyncLocalStorage request context
telemetry/ Application Insights behind a vendor-neutral port
errors/ Error hierarchy, codes, normaliser
schemas/ Shared Zod schemas
utils/ Identifiers, timing, backoff
types/ Shared type declarations
tests/
unit/ integration/ mocks/ helpers/
docs/
Getting started
Prerequisites
- Node.js 22 LTS (the toolchain also runs on Node 24)
- npm 10+
- A Shopify app created in the Dev Dashboard, installed on your store
Install and run
npm install
cp .env.example .env # then fill in the Shopify values
npm run dev # watch mode
npm run build && npm start # production build
Verify
npm run verify # typecheck + lint + tests
npm run test:coverage # coverage report
curl http://localhost:8080/health
curl http://localhost:8080/ready
curl http://localhost:8080/version
MCP handshake:
curl -X POST http://localhost:8080/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
"protocolVersion":"2025-06-18","capabilities":{},
"clientInfo":{"name":"curl","version":"1.0"}}}'
Getting the Shopify credentials
Shopify has retired legacy custom apps. The store admin no longer offers "Create an app", and there is no long-lived Admin API token to copy anywhere. Apps are now created in the Shopify Dev Dashboard, and credentials are exchanged for a short-lived access token through the OAuth client credentials grant.
- Create an app in the Dev Dashboard and install it on your store.
- Open your app → Settings → Credentials and copy the Client ID and Secret (
shpss_…). - Set them as
SHOPIFY_CLIENT_IDandSHOPIFY_CLIENT_SECRET.
The server exchanges them for a token at startup and renews it automatically. Verify manually with:
curl -X POST https://<store>.myshopify.com/admin/oauth/access_token \
-d grant_type=client_credentials \
-d client_id=<CLIENT_ID> -d client_secret=<CLIENT_SECRET>
# -> {"access_token":"shpat_…","scope":"…","expires_in":86399}
expires_inis 24 hours. This is why a pastedSHOPIFY_ADMIN_ACCESS_TOKENis rejected in production: it works the day it is set and starts returning HTTP 401 the next.
The grant requires the app and the store to be in the same Shopify organization. If they are not, Shopify answers
invalid_client— which looks identical to a wrong secret.
Required read scopes: read_products, read_inventory, read_locations, read_orders,
read_all_orders, read_customers, read_discounts, read_price_rules, read_analytics,
read_marketing_events.
Grant read scopes only. This server is read-only and advertises
readOnlyHint: true, but the credential itself is not constrained by that — an app grantedwrite_productsholds the ability to delete a catalogue, on a connection whose other end is an LLM.
Configuration
Every variable is validated at startup. A misconfigured deployment fails immediately with every offending variable listed at once, rather than failing on the first one or booting half-configured.
See .env.example for the full annotated contract. The essentials:
| Variable | Required | Default | Purpose |
|---|---|---|---|
SHOPIFY_STORE |
Yes | — | Store handle or *.myshopify.com domain |
SHOPIFY_CLIENT_ID |
Yes¹ | — | Dev Dashboard app client ID |
SHOPIFY_CLIENT_SECRET |
Yes¹ | — | Dev Dashboard app secret (shpss_…) |
SHOPIFY_ADMIN_ACCESS_TOKEN |
Local dev only¹ | — | Pre-minted token; expires in 24h |
SHOPIFY_API_VERSION |
No | 2026-07 |
Quarterly Admin API version |
PORT |
No | 8080 |
Injected by App Service |
MCP_HTTP_PATH |
No | /mcp |
Must match the connector's declared path |
MCP_API_KEY |
In production | — | Shared secret for the connector |
MCP_ALLOWED_HOSTS |
In production | — | DNS-rebinding protection allowlist |
APPINSIGHTS_CONNECTION_STRING |
In production | — | Application Insights |
LOG_PRETTY |
Must be false in production |
false |
Human-readable logs (local only) |
¹ Supply either the client credentials pair or a static token. Startup fails if neither is
present. When both are given, client credentials win — they are the only renewable option — and the
active mode is stated in the startup log and on GET /version.
Production invariants
The server refuses to start in NODE_ENV=production when any of these hold:
- no
MCP_API_KEY— an unauthenticated MCP endpoint exposes the store's entire order and customer dataset to anonymous callers; - no
APPINSIGHTS_CONNECTION_STRING; - no
MCP_ALLOWED_HOSTS; LOG_PRETTY=true;- a static
SHOPIFY_ADMIN_ACCESS_TOKENinstead of client credentials — it would expire within 24 hours with no way to renew itself.
These are configuration errors, not warnings.
Operational endpoints
| Endpoint | Question answered | Behaviour |
|---|---|---|
/health |
Is the process alive? | Always 200 while running. No I/O. |
/ready |
Can this instance serve a request? | 200 / 503 based on a cached, 1-cost-point Shopify probe. |
/version |
Which build, store, API version, tools? | Build identity and the published tool catalogue. |
Configure the App Service health check against
/health, not/ready. Pointing the platform probe at a dependency-aware endpoint makes App Service recycle healthy workers during a Shopify incident, turning a degraded service into an outage. Use/readyfor deployment gates and load balancer decisions.
Design decisions worth knowing
Stateless MCP sessions by default. A fresh McpServer is built per request, so any App Service
instance can serve any request and scale-out needs no ARR affinity. MCP_SESSION_MODE=stateful is
available but requires session affinity.
Tokens are minted and renewed by the server. Shopify's client credentials grant issues 24-hour tokens. The provider caches one in memory, renews it 5 minutes before expiry, and collapses concurrent callers onto a single in-flight mint so a burst of tool calls on a cold instance does not trigger a token request each. If Shopify rejects a token early — after a credential rotation — the client discards it and retries exactly once, outside the retry budget, so recovery also works for callers that disable retries (such as the readiness probe).
Client-side cost governance. Shopify meters GraphQL by query cost against a leaky bucket
(1000 points refilling at 50/s on standard plans; the live test store reported 4000 at 200/s). The
client reads extensions.cost from every response, adopts whatever limits the store actually
reports, and waits for headroom before dispatching — rather than absorbing a THROTTLED error and
retrying into an empty bucket.
Search filters come from an allowlist. Shopify silently ignores filter fields it does not
recognise: price:<50 filters correctly, while variants.price:<50 is dropped and returns
unfiltered products with HTTP 200 and no error at all. An agent would then relay those results as
though they satisfied the filter. Every field name the query builder emits is drawn from a fixed
list verified against the live API, and values are escaped and quoted.
Stock figures are reported honestly. Shopify returns totalInventory: null for untracked
products, which is not the same as zero in stock, so the domain model keeps a separate
inventoryTracked flag. Live data also showed a product reporting totalInventory: 0 while its
variants stood at -31, -19 and -23; the summary states the oversell rather than repeating the zero.
Throttling arrives as HTTP 200. The Admin API reports throttling as a successful HTTP response
with errors[].extensions.code === "THROTTLED". The client checks the GraphQL error array and the
HTTP status, and classifies each failure mode for retryability independently.
Tool failures are returned, not thrown. A thrown error becomes a JSON-RPC protocol error, which
Copilot Studio surfaces as an opaque connector failure the agent cannot reason about. Tool failures
are returned as isError: true results carrying the reason, retryability, remediation and
correlation ID, so the agent can explain the problem to the user.
Uniform response envelope. Every tool returns { summary, resultCount, truncated, data }.
summary gives the agent a grounded sentence it can relay verbatim; truncated lets it state
honestly that a figure is a bounded sample rather than a store-wide total.
Correlation without plumbing. Correlation IDs propagate through AsyncLocalStorage, so the
domain layer carries no context parameters, yet every log line, telemetry item and Shopify call is
attributable to the originating request.
Copilot Studio compatibility
- Streamable HTTP only. Copilot Studio dropped SSE transport support in August 2025.
- Custom connector with
x-ms-agentic-protocol: mcp-streamable-1.0on a POST operation. - HTTPS, terminated by App Service.
- Accept header shim. The MCP specification requires clients to accept both
application/jsonandtext/event-stream. Power Platform connectors may forward onlyapplication/json, which the SDK rejects with HTTP 406 — surfacing in Copilot Studio as an unexplained connector failure. The MCP route widens the header (onrawHeaders, which is what the transport actually reads). - Tool metadata is validated at registration:
snake_casenames and descriptions long enough to drive orchestrator routing. Both input and output schemas are published.
Connector setup, the OpenAPI schema and Azure deployment steps are documented in a later milestone.
Testing
npm test # all suites
npm run test:unit
npm run test:integration
npm run test:coverage
Integration tests drive the real Fastify instance via app.inject() — correlation hooks, security
plugins, error handler, route wiring and the MCP SDK transport all execute; only the network is
absent. Shopify is substituted at the transport seam, so retry sequencing, cost accounting and error
mapping are exercised for real.
Scripts
| Script | Purpose |
|---|---|
npm run dev |
Watch mode via tsx |
npm run build |
Clean + compile to dist/ |
npm start |
Run the compiled server |
npm run verify |
Typecheck + lint + test |
npm run typecheck |
tsc --noEmit |
npm run lint |
ESLint, zero warnings tolerated |
npm run format |
Prettier |
Troubleshooting
| Symptom | Cause and fix |
|---|---|
| Startup fails listing environment variables | Working as designed. Fix every listed variable; all problems are reported at once. |
/ready reports SHOPIFY_AUTHENTICATION_FAILED |
Wrong client credentials, app and store in different Shopify organizations, or a missing read scope. |
/ready reports SHOPIFY_API_ERROR with a 404 hint |
SHOPIFY_STORE or SHOPIFY_API_VERSION is wrong; an unsupported version returns 404. |
HTTP 406 from /mcp |
The client sent a narrow Accept header. The shim handles the common case; check for a proxy rewriting headers. |
HTTP 401 from /mcp |
MCP_API_KEY mismatch. The connector must send it as x-api-key or Authorization: Bearer. |
Frequent SHOPIFY_THROTTLED warnings |
The store's cost budget is under pressure. Raise SHOPIFY_MIN_COST_BUFFER or lower page sizes. |
| Logs are unreadable JSON locally | Set LOG_PRETTY=true. It is rejected in production, where JSON is required. |
Every error response and tool failure carries a correlation ID. Search Application Insights on
customDimensions.correlationId to retrieve the full request trace, including each Shopify call,
its cost and its outcome.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。