Shopify MCP Engineering Notes

Engineering Case Study

Building a Shopify MCP Connector: Components, Challenges and Architecture

By Arjun Chatterjee · July 20, 2026 · 13 min read

Component architecture for Shopify merchant OAuth, MCP clients, tenant credentials and Shopify GraphQL

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.

What this article covers: the design and current repository implementation, including trade-offs that remain. It is an engineering note—not a product announcement, a Shopify certification, or a claim that the connector directly integrates with inriver.

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_shop returns basic store identity.
  • list_products provides a bounded, cursor-paginated catalogue view.
  • get_product resolves a numeric ID or Shopify product GID and returns product detail.
  • list_orders provides 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 GraphQL

Useful 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 GraphQL

Useful 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

  1. Normalize the shop: the install route accepts only a valid *.myshopify.com domain shape, reducing open-redirect and foreign-host ambiguity.
  2. Bind the OAuth request: the service generates random state, stores its digest with the shop and a ten-minute expiry, then redirects to Shopify.
  3. 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.
  4. 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.
  5. Authorize the MCP call: the bearer token maps the request to one installation. The MCP client never receives the Shopify Admin token.
  6. Refresh when needed: expiring offline credentials are refreshed shortly before expiry. A per-store asyncio.Lock prevents two requests in one process from racing Shopify’s single-use refresh token.
  7. Execute a fixed tool: the selected tool constructs a predetermined GraphQL document, validates identifiers and caps list size at 50.
  8. Handle failure deliberately: network errors, HTTP 429/5xx and GraphQL THROTTLED responses retry with exponential backoff and jitter; other GraphQL errors surface without leaking the token.
  9. 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 fitRequires more design
Read-only product discovery and support assistantsPrice, inventory or catalogue mutations
Checking Shopify’s published state after PIM syndicationFull PIM-to-commerce reconciliation
Small internal workflows using local stdioEnd-user delegated authorization and granular user consent
Single-node hosted service for controlled tenantsMulti-region or horizontally scaled tenancy
Grounding a larger RAG or agent workflow with live commerce factsBulk analytical extraction or warehouse replacement

Security and production assessment

Positive controls in the current code: fixed read-only tools, tenant identity derived from authentication, short-lived single-use OAuth state, constant-time HMAC checks, encrypted Shopify credentials, digested MCP tokens, bounded pagination, timeout/retry policy and verified uninstall cleanup.
Production boundary: this is a deployable single-node baseline, not a security or compliance certification. An internet-facing deployment still needs TLS termination, WAF and rate limits, request-size controls, managed secrets, alerting, restricted metrics access, dependency/container scanning, Shopify compliance webhooks, protected-customer-data review where applicable, privacy policy and an incident/rotation runbook.

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

  1. Replace SQLite with a managed relational store and transactional token rotation.
  2. Add a standards-based authorization server flow for MCP clients that cannot accept provisioned bearer tokens.
  3. Introduce tool-level scope policy so product and order access can be separated per installation.
  4. Add nested variant pagination and explicit cost budgets for multi-page agent calls.
  5. Export OpenTelemetry traces alongside metrics, with sensitive-field redaction tests.
  6. Add contract tests against Shopify API-version changes and a scheduled upgrade policy.
  7. Design a separate inriver comparison tool only when both source and channel schemas, identity mapping and governance rules are available.

Repository and implementation references

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.