Shopify Admin GraphQL Gateway MCP
# Shopify Admin GraphQL Gateway MCP
A security-focused MCP stdio server for a restricted LibreChat agent. It obtains short-lived Shopify Admin API tokens internally, permits read-only GraphQL by default, and puts mutations behind a preview plus one-time confirmation flow.
The gateway does not search documentation itself. Configure Shopify's separate Dev MCP beside it so the agent can search current docs and introspect the selected Admin API schema before sending a query to this gateway:
```text
LibreChat agent
├─ Shopify Dev MCP (documentation and schema; no store credentials)
└─ Shopify Admin Gateway (fixed store and API version; credentials stay server-side)
```
## Security model
- The store origin is always `https://<shop>.myshopify.com`. The model cannot provide a domain, URL, API version, client credential, or access token.
- Shopify client credentials and cached access tokens exist only in the gateway process. They are never tool inputs or outputs.
- `shopify_admin_graphql_query` parses the GraphQL document and only accepts a selected `query` operation. It rejects mutations and subscriptions.
- Tool schemas are strict. There is no generic HTTP, REST, shell, filesystem, or environment-reading tool.
- Token scopes must exactly match `SHOPIFY_ALLOWED_SCOPES`. This prevents a broader app token from silently widening the gateway's authority.
- Mutation execution defaults off. A preview is bound server-side to the authenticated LibreChat user, normalized query hash, canonical variables hash, configured store, and expiry. Tokens are HMAC-authenticated, one-time, and held only in memory. LibreChat's native human-in-the-loop policy requires approval before it invokes the execute tool.
- Audit logs contain operation/resource identifiers and outcome, not mutation variables, secrets, or full customer data.
- Cursor pagination is opt-in, bounded by page size, page count, and an overall timeout. Rate-limit extensions are preserved.
- Product counts use `productsCount`; they are never inferred from a paginated product connection.
The process never loads `.env` or `/opt/data/.env`. Supply environment variables through the process supervisor or container orchestrator.
## Shopify app setup
The OAuth client-credentials grant works only for an app developed by the same Shopify organization that owns the store and installed on that store. Public and externally owned custom apps must use a different OAuth flow; this gateway intentionally does not implement those flows.
1. In Shopify's Dev Dashboard, create an app owned by your organization.
2. Configure only the Admin API scopes the gateway needs.
3. Release/install that app on the target store.
4. Copy the app's client ID and client secret into the server-side deployment environment.
For the read-only example configuration, grant exactly:
```text
read_products,read_inventory,read_orders
```
The token endpoint's returned scope set must exactly match `SHOPIFY_ALLOWED_SCOPES`. If you later enable a mutation, add only its documented write scope to both the app and the allowlist. Preview uses an exact-name catalog and rejects every mutation name it does not recognize; it does not infer scope from a broad prefix. The initial catalog covers `productCreate`, `productUpdate`, `productDelete`, `productSet`, `inventoryAdjustQuantities`, `inventorySetQuantities`, `orderUpdate`, `draftOrderCreate`, `customerUpdate`, `discountCodeBasicCreate`, `fileCreate`, and `metaobjectCreate`. Verify each catalog result against Shopify Dev MCP/current documentation whenever the API version changes.
## Environment variables
| Variable | Required/default | Purpose |
| --- | --- | --- |
| `SHOPIFY_STORE_DOMAIN` | Required | Accepts `your-store` or `your-store.myshopify.com`; schemes, paths, ports, and other domains are rejected. |
| `SHOPIFY_CLIENT_ID` | Required | Server-side Shopify app client ID. |
| `SHOPIFY_CLIENT_SECRET` | Required | Server-side Shopify app secret. Never expose it to LibreChat's model or Code Interpreter. |
| `SHOPIFY_API_VERSION` | `2026-07` | Fixed quarterly Admin API version. Must use `YYYY-01`, `YYYY-04`, `YYYY-07`, or `YYYY-10`. |
| `SHOPIFY_ALLOWED_SCOPES` | Required | Comma-separated exact scope allowlist. |
| `SHOPIFY_ENABLE_MUTATIONS` | `false` | Mutation execution kill switch. Preview remains available. |
| `SHOPIFY_TOKEN_REFRESH_BUFFER_SECONDS` | `300` | Refresh before token expiry. |
| `SHOPIFY_REQUEST_TIMEOUT_MS` | `30000` | Overall request/pagination deadline, from 1–120 seconds. |
| `SHOPIFY_MAX_PAGE_SIZE` | `100` | Maximum `first` or `last`, capped at 250. |
| `SHOPIFY_MAX_PAGES` | `10` | Maximum pages in one tool call, capped at 100. |
| `SHOPIFY_CONFIRMATION_TTL_SECONDS` | `300` | Mutation confirmation lifetime, from 30–900 seconds. |
| `SHOPIFY_LIBRECHAT_USER_ID` | Empty | Trusted authenticated user identity. Required for mutation preview/execution. |
| `SHOPIFY_LIBRECHAT_AGENT_ID` | Empty | Trusted active-agent identity. Required for mutation preview/execution and recorded in audits. |
See [.env.example](.env.example). It is a template only; the application does not load it.
## Local development
Node.js 20 or newer is required. Every build dependency is pinned and the lockfile fixes the transitive dependency graph. The published CLI is a single bundled executable with no runtime package dependencies.
```bash
npm ci --ignore-scripts
npm run check
```
For a local stdio run, export the variables in your shell and run:
```bash
npm run build
npm start
```
Do not type protocol messages into the stdio process manually. Use an MCP client or Inspector. Logs go only to stderr because stdout is reserved for MCP.
## Tools
### `shopify_admin_graphql_query`
Required input: `query`. Optional inputs: `variables`, `operationName`, and bounded `pagination`.
For automatic pagination, the query must use variable-backed `first` and `after` arguments and select `pageInfo { hasNextPage endCursor }`. Tell the gateway where the connection appears below `data`:
```json
{
"query": "query Products($first: Int!, $after: String) { products(first: $first, after: $after) { nodes { id title } pageInfo { hasNextPage endCursor } } }",
"operationName": "Products",
"variables": {},
"pagination": {
"connectionPath": ["products"],
"pageSize": 50,
"maxPages": 3
}
}
```
The response includes `data`, `errors`, `extensions`, `httpStatus`, `apiVersion`, `operationName`, and Shopify's request ID when present. Preserving `httpStatus` ensures a non-2xx GraphQL envelope cannot be audited as a successful mutation. Paginated results also include `pagesFetched`, `partial`, `limitReached`, `hasNextPage`, and rate-limit snapshots. The gateway concatenates `nodes` and/or `edges` at the specified connection path.
Do not paginate for counts. Use Shopify count fields or `shopify_admin_product_counts`.
### `shopify_admin_product_counts`
Runs five aliased `productsCount(limit: null, ...)` fields for total, active, draft, archived, and unlisted products. Explicit `limit: null` avoids Shopify's default 10,000-count cap. It returns each count and Shopify's precision.
### Mutation approval flow
1. The agent searches Shopify Dev MCP and prepares exactly one top-level mutation.
2. Call `shopify_admin_graphql_preview_mutation`. Nothing is sent to Shopify.
3. The user reviews the mutation name, complete requested variables, required scope catalog result, resource IDs, and change summary.
4. Call `shopify_admin_graphql_execute_mutation` with only the confirmation token. LibreChat interrupts the agent before invoking the tool and shows its native approve/reject UI.
5. After explicit approval, the gateway checks the kill switch, authenticated user/agent, token MAC, expiry, store binding, query hash, variables hash, and one-time preview record before sending the stored mutation.
Direct execution is impossible because the execute tool accepts no query or variables. A token is consumed before execution and cannot be replayed, including after a failed Shopify request.
The supplied LibreChat configuration enables its durable human-in-the-loop `toolApproval` policy, allows the read/preview tools, and places only `shopify_admin_graphql_execute_mutation` on the `ask` list. It uses the exact normalized runtime name LibreChat assigns to that MCP tool. In `@librechat/agents` 3.4.x, an explicit `ask` rule wins over `allow` and every mode except `deny`, including `bypass`. MongoDB checkpoints allow a paused approval to resume. Pin and retest LibreChat before enabling mutations or upgrading it.
## LibreChat configuration
The complete configuration is in [config/librechat.yaml](config/librechat.yaml). The gateway block is:
```yaml
mcpServers:
shopify-admin-gateway:
type: stdio
command: npx
args:
- -y
- 'shopify-admin-graphql-gateway-mcp@1.0.0'
env:
SHOPIFY_STORE_DOMAIN: '${SHOPIFY_STORE_DOMAIN}'
SHOPIFY_CLIENT_ID: '${SHOPIFY_CLIENT_ID}'
SHOPIFY_CLIENT_SECRET: '${SHOPIFY_CLIENT_SECRET}'
SHOPIFY_API_VERSION: '${SHOPIFY_API_VERSION}'
SHOPIFY_ALLOWED_SCOPES: '${SHOPIFY_ALLOWED_SCOPES}'
SHOPIFY_ENABLE_MUTATIONS: '${SHOPIFY_ENABLE_MUTATIONS}'
SHOPIFY_TOKEN_REFRESH_BUFFER_SECONDS: '${SHOPIFY_TOKEN_REFRESH_BUFFER_SECONDS}'
SHOPIFY_LIBRECHAT_USER_ID: '{{LIBRECHAT_USER_ID}}'
SHOPIFY_LIBRECHAT_AGENT_ID: '${SHOPIFY_LIBRECHAT_AGENT_ID}'
chatMenu: false
timeout: 60000
initTimeout: 30000
serverInstructions: |
Use Shopify documentation tools first when available.
Use read-only Shopify queries by default.
Never execute mutations without preview and explicit confirmation.
Never expose credentials or tokens.
```
The companion Shopify Dev MCP is pinned in the supplied configuration as `@shopify/dev-mcp@1.14.4`. It has no store credentials and cannot execute against the store.
LibreChat exposes MCP tools to its policy layer as `<tool>_mcp_<normalized-server>`. Therefore the protected execute name is `shopify_admin_graphql_execute_mutation_mcp_shopify-admin-gateway`; a policy string in the form `mcp:<server>:<tool>` does not match. The supplied exact `ask` rule enforces the correct name.
`{{LIBRECHAT_USER_ID}}` causes LibreChat to create a user-scoped stdio process, allowing that user's in-memory preview record to remain available during approval. Set `SHOPIFY_LIBRECHAT_AGENT_ID` to the immutable ID of the one restricted agent receiving these tools, and enforce that assignment with LibreChat ACLs. Treat both generated values as trusted identity assertions: users and models must not be able to edit the MCP definition. Mutations fail closed if either identity is absent or unresolved.
Keep `chatMenu: false`, assign both MCP servers only to the restricted agent through LibreChat's role/resource ACLs, prevent normal users from adding arbitrary MCP servers, and keep Code Interpreter credentials/filesystem separate from the LibreChat API process.
### Publish and install with npx
The repository is publish-ready under the currently available unscoped name `shopify-admin-graphql-gateway-mcp`. Publishing is deliberately not automated because it changes external state:
```bash
npm login
npm run check
npm publish
```
An unscoped npm package is public. The package contains executable code and documentation only—never Shopify credentials—but review your organization's policy before publishing. Its `UNLICENSED` declaration is intentional until the owner chooses a source license; make that choice explicitly before a public release. For a private package, first change `name` in `package.json` to a scope your organization owns, such as `@your-company/shopify-admin-gateway`, set `publishConfig.access` to `restricted`, update the exact package name in `config/librechat.yaml`, and configure registry authentication only in the LibreChat API container.
The `prepack` lifecycle rebuilds and type-checks the package. `npm run package:check` creates the real tarball, verifies that source, tests, configuration, and environment files are excluded, installs it without lifecycle scripts or network dependencies, and executes the installed CLI through its generated npm binary.
After publishing, the supplied configuration needs no custom LibreChat image: `npx -y shopify-admin-graphql-gateway-mcp@1.0.0` downloads and runs the exact package version inside the LibreChat API process. Keep the package version pinned and review upgrades before changing it. This convenience depends on npm registry availability and gives the credential-holding LibreChat API container outbound registry access; use a controlled registry/cache or the preinstalled Docker alternative below when production policy requires a smaller supply-chain surface.
## Docker deployment
Build the pinned, non-root runtime image:
```bash
docker build -t shopify-admin-gateway:1.0.0 .
```
Docker remains an alternative for environments that do not permit runtime npm downloads. Because stdio MCP servers are child processes, the gateway must be present inside the LibreChat API container that launches it. Do not mount the Docker socket or a host source directory. In your controlled LibreChat API Dockerfile, use this repository's build stage and copy the immutable runtime tree into `/app/shopify-admin-gateway`, then change the gateway MCP command from `npx` back to `node /app/shopify-admin-gateway/dist/index.js`:
```dockerfile
FROM shopify-admin-gateway:1.0.0 AS shopify_gateway
# Replace this line with your pinned LibreChat API image.
FROM your-pinned-librechat-api-image
USER root
COPY --from=shopify_gateway --chown=node:node /app/shopify-admin-gateway /app/shopify-admin-gateway
USER node
```
Inject Shopify variables through your secret manager/orchestrator into the LibreChat API container. Do not bake them into either image, place them in `librechat.yaml`, mount a host `.env`, or expose them to the Code Interpreter container.
## Read-only validation
Start with `SHOPIFY_ENABLE_MUTATIONS=false`, then verify:
1. `shopify_admin_product_counts` returns plausible values and precision.
2. A small query such as `query ShopName { shop { name } }` succeeds.
3. A mutation sent to `shopify_admin_graphql_query` returns `MUTATION_REJECTED`.
4. Mutation preview returns a token but execute returns `MUTATIONS_DISABLED`.
5. Logs and tool responses contain neither the client secret nor the access token.
Run the automated suite with `npm test`. It covers token acquisition/caching/refresh, invalid credentials/domain, query responses/errors, mutation rejection/preview/confirmation/execution, scope enforcement, product statuses, pagination limits, rate-limit retry/preservation, and secret redaction.
## Audit logs
Each mutation attempt that passes confirmation validation writes one JSON line to stderr:
```json
{"level":"audit","event":"shopify_mutation","timestamp":"2026-08-07T16:00:00.000Z","userId":"...","agentId":"...","operationName":"UpdateProduct","mutationName":"productUpdate","resourceIds":["gid://shopify/Product/123"],"success":true}
```
Send stderr to your centralized log collector with access controls and retention appropriate for operational audit data. Query payloads, mutation variables, response bodies, secrets, and customer records are deliberately excluded.
## Security limitations
- Scope control is the primary authorization boundary. A valid read query can access any field allowed by the app's scopes; this version does not implement a per-field or per-resource policy engine.
- `read_orders` and similar scopes can expose personal data. Give the LibreChat agent only the scopes and audience it genuinely needs, and apply data retention controls to chat histories.
- Stdio MCP has no standard end-user authentication of its own. Mutation identity relies on LibreChat's trusted user-scoped environment substitution and locked administrator configuration.
- A confirmation token proves preview continuity, not human intent by itself. Human intent is enforced by LibreChat's native HITL `ask` rule before the MCP call. Do not expose the mutation execute tool through a host that lacks an equivalent approval boundary.
- Confirmation state and Shopify tokens are in memory. A restart safely invalidates confirmations and causes a fresh Shopify token exchange.
- Required scopes come from a deliberately small exact-mutation-name catalog; unsupported names fail closed. Verify catalog entries through Shopify Dev MCP/current Shopify documentation whenever the API version changes.
- Automatic pagination supports one explicitly identified connection and merges its `nodes`/`edges`. Complex multi-connection queries should be split into smaller operations.
- Rate-limit responses can still occur. Read queries retry one HTTP 429 within the total timeout; mutations are never automatically retried because doing so could duplicate a write.
- This version intentionally has no arbitrary REST facility. Add a narrowly scoped REST operation only for a documented GraphQL gap.
## References
- [Shopify client-credentials grant](https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens/client-credentials-grant)
- [Shopify Admin GraphQL API](https://shopify.dev/docs/api/admin-graphql)
- [Shopify Dev MCP / AI toolkit](https://shopify.dev/docs/apps/build/ai-toolkit)
- [Model Context Protocol TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)
- [LibreChat MCP server configuration](https://www.librechat.ai/docs/configuration/librechat_yaml/object_structure/mcp_servers)
- [LibreChat human-in-the-loop tool approval](https://www.librechat.ai/docs/features/agents/human_in_the_loop)
TDQS
Scored across 4 tools
Each tool has a clearly distinct purpose: product counts, read-only queries, mutation preview, and mutation execution. There is no overlap or ambiguity between them.
All tools share the consistent 'shopify_admin' prefix and use snake_case. The three GraphQL tools follow a clear 'graphql_<action>' pattern, but 'product_counts' deviates as a noun phrase rather than a verb_noun structure.
With only 4 tools, the server is tightly scoped to its purpose as a GraphQL gateway. Each tool is necessary and covers a distinct aspect of the interaction model.
The set provides complete lifecycle coverage for the gateway domain: read operations via query, mutation handling via preview and execute, and a convenience count tool. No obvious missing operations within the stated scope.