I built Shopify MCP Simple to answer a narrow architectural question: how can an AI client inspect current commerce data without receiving a general Shopify GraphQL console or a long-lived Admin API credential?
The result is deliberately small: four read-only tools, two runtime modes, and a hosted security layer around Shopify OAuth. The interesting work was not exposing a product query. It was defining tenant identity, token boundaries, retry behaviour, pagination, lifecycle cleanup and an honest production boundary.
The problem boundary
A useful MCP server should reduce an AI agent’s authority, not merely repackage an existing API. Shopify’s Admin GraphQL API is broad and mutation-capable. Passing arbitrary GraphQL through MCP would let a prompt decide query shape, resource scope and cost. I chose fixed tools instead:
get_shopreturns basic store identity.list_productsprovides a bounded, cursor-paginated catalogue view.get_productresolves a numeric ID or Shopify product GID and returns product detail.list_ordersprovides a bounded operational view when the installation has the required scope.
There are no write tools. Price, inventory and content mutations require approval, audit and idempotency patterns that should not be hidden inside a generic assistant action.
Component map
MCP server
FastMCP registers the same four tool functions for local and hosted operation. The tool layer owns fixed GraphQL documents and input normalization.
Shopify client
An asynchronous HTTP client adds the Admin token, timeout policy, API version and retry behaviour. It separates transport failures from GraphQL errors and returns throttle metadata.
Hosted web application
OAuth install/callback routes, uninstall webhook, Streamable HTTP MCP endpoint, request metrics and health probes share one ASGI application.
Credential store
SQLite stores installations, short-lived OAuth state and MCP-token digests. Shopify access and refresh tokens are encrypted with Fernet before storage.
Token verifier
The incoming MCP bearer token is digested, resolved to an installation and attached to request context. The original MCP token is shown only at installation time.
Operations surface
/healthz, /readyz, /metrics and structured JSON request logs expose liveness, database readiness and basic traffic signals.
Two setups, one tool contract
1. Local, single-store, stdio
The simplest path uses Python 3.11+, a shop domain and a Shopify Admin API token supplied through environment variables. The MCP client launches shopify-mcp over stdio.
SHOPIFY_SHOP=store.myshopify.com
SHOPIFY_ACCESS_TOKEN=shpat_...
SHOPIFY_API_VERSION=2026-07
MCP client → stdio process → Shopify Admin GraphQLUseful for: development, one-person workflows and testing a store integration without hosting OAuth infrastructure.
Limitation: the operator provisions and protects the Shopify token; this is not merchant self-service.
2. Hosted, multi-tenant, Streamable HTTP
The hosted path adds merchant OAuth, encrypted credentials and an installation-specific MCP bearer token. A remote client sends that bearer token to /mcp.
Merchant → /install → Shopify consent → /auth/callback
MCP client → Bearer smcp_... → /mcp
Service → resolve tenant → Shopify GraphQLUseful for: repeatable installations and remote MCP clients that can supply a bearer credential.
Limitation: it is an OAuth resource server with provisioned bearer credentials, not a full interactive OAuth authorization server for every possible MCP client.
Hosted request flow, step by step
- Normalize the shop: the install route accepts only a valid
*.myshopify.comdomain shape, reducing open-redirect and foreign-host ambiguity. - Bind the OAuth request: the service generates random state, stores its digest with the shop and a ten-minute expiry, then redirects to Shopify.
- Validate the callback: callback HMAC is reconstructed from sorted query parameters and compared in constant time. State must match the shop, be unexpired and is consumed once.
- Protect credentials: access and optional refresh tokens are encrypted before persistence. A new MCP token is returned once; only its SHA-256 digest is stored.
- Authorize the MCP call: the bearer token maps the request to one installation. The MCP client never receives the Shopify Admin token.
- Refresh when needed: expiring offline credentials are refreshed shortly before expiry. A per-store
asyncio.Lockprevents two requests in one process from racing Shopify’s single-use refresh token. - Execute a fixed tool: the selected tool constructs a predetermined GraphQL document, validates identifiers and caps list size at 50.
- Handle failure deliberately: network errors, HTTP 429/5xx and GraphQL
THROTTLEDresponses retry with exponential backoff and jitter; other GraphQL errors surface without leaking the token. - Clean up: the uninstall webhook verifies the raw-body HMAC before deleting tenant credentials.
Why the inriver relationship matters
inriver enrichment and approval
↓
channel syndication / transformation
↓
Shopify published commerce state
↓
fixed MCP read tools
↓
AI response grounded in retrieved channel data
In this pattern, inriver remains the source of governed product information. Its channel logic determines which approved fields and structures reach Shopify. The MCP connector sits after that boundary and reads what the commerce channel currently exposes.
This makes the connector useful for publication verification, product and variant summaries, inventory-aware questions, merchandising support and downstream agents that require current channel context. It does not prove that Shopify matches every PIM attribute. A reconciliation use case would need both the inriver source representation and the Shopify result, plus explicit mapping and comparison logic.
Development challenges and the choices behind them
1. Tenant identity cannot come from a tool argument
Allowing a model to pass shop=another-store.myshopify.com would create a tenant-confusion risk. In hosted mode, shop identity comes from the authenticated bearer token and server-side installation record. Tool inputs describe the requested resource, not the tenant.
2. Refresh-token rotation is a concurrency problem
When refresh tokens are single-use, simultaneous requests can both observe an expired credential and attempt rotation. The process-local per-store lock solves this for one instance. It does not solve it across replicas; distributed deployment needs a database-backed lease, transaction or equivalent shared lock.
3. GraphQL success is not always HTTP success
Shopify can return HTTP 200 with a GraphQL error or throttle signal. The client therefore inspects the response body, distinguishes retryable throttling from domain errors and includes throttleStatus so callers can observe API capacity.
4. Pagination must be an MCP contract
An assistant may ask for “all products,” but fetching an unbounded catalogue is expensive and unsafe. List tools cap the requested page size and return page_info.endCursor. The client—or the agent under an orchestration budget—decides whether another page is justified.
5. Secrets have different lifecycles
The Shopify client secret, encryption key, Shopify access token and MCP bearer token are not interchangeable. They differ in issuer, audience, storage and rotation. The code keeps Shopify credentials server-side, encrypts them at rest and hashes the MCP lookup credential. Production still needs a managed secret store and an encryption-key rotation procedure.
Usefulness and non-goals
| Good fit | Requires more design |
|---|---|
| Read-only product discovery and support assistants | Price, inventory or catalogue mutations |
| Checking Shopify’s published state after PIM syndication | Full PIM-to-commerce reconciliation |
| Small internal workflows using local stdio | End-user delegated authorization and granular user consent |
| Single-node hosted service for controlled tenants | Multi-region or horizontally scaled tenancy |
| Grounding a larger RAG or agent workflow with live commerce facts | Bulk analytical extraction or warehouse replacement |
Security and production assessment
SQLite and the in-process refresh lock intentionally keep the example understandable. They are also the clearest scaling constraint. Multiple replicas need a shared durable store and distributed refresh coordination. Product detail currently retrieves only the first 25 variants, so large variant catalogues need nested pagination. Order access carries a different privacy profile from product access and should be scoped, reviewed and potentially separated by deployment.
What I would build next
- Replace SQLite with a managed relational store and transactional token rotation.
- Add a standards-based authorization server flow for MCP clients that cannot accept provisioned bearer tokens.
- Introduce tool-level scope policy so product and order access can be separated per installation.
- Add nested variant pagination and explicit cost budgets for multi-page agent calls.
- Export OpenTelemetry traces alongside metrics, with sensitive-field redaction tests.
- Add contract tests against Shopify API-version changes and a scheduled upgrade policy.
- Design a separate inriver comparison tool only when both source and channel schemas, identity mapping and governance rules are available.
Repository and implementation references
- Shopify MCP Simple repository
- MCP tools, tenant context and token refresh
- Hosted OAuth, webhooks, probes and metrics
- Encrypted installation and token storage
- Shopify GraphQL transport and retry policy
- Deployment and production checklist
The implementation and observations reflect repository version 0.2.0 reviewed July 20, 2026. Validate Shopify API versions, platform policies and MCP client authentication requirements for your environment.
