-
Notifications
You must be signed in to change notification settings - Fork 228
Add @shopify/mcp — MCP server for Admin API #6923
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| import {identityFqdn} from './context/fqdn.js' | ||
| import {BugError} from './error.js' | ||
| import {shopifyFetch} from './http.js' | ||
| import {clientId, applicationId} from '../../private/node/session/identity.js' | ||
| import {pollForDeviceAuthorization} from '../../private/node/session/device-authorization.js' | ||
| import {exchangeAccessForApplicationTokens, ExchangeScopes} from '../../private/node/session/exchange.js' | ||
| import {allDefaultScopes, apiScopes} from '../../private/node/session/scopes.js' | ||
| import * as sessionStore from '../../private/node/session/store.js' | ||
| import {setCurrentSessionId} from '../../private/node/conf-store.js' | ||
|
|
||
| import type {AdminSession} from './session.js' | ||
|
|
||
| export interface DeviceCodeResponse { | ||
| deviceCode: string | ||
| userCode: string | ||
| verificationUri: string | ||
| verificationUriComplete: string | ||
| expiresIn: number | ||
| interval: number | ||
| } | ||
|
|
||
| /** | ||
| * Requests a device authorization code for MCP non-interactive auth. | ||
| * | ||
| * @returns The device code response with verification URL. | ||
| */ | ||
| export async function requestDeviceCode(): Promise<DeviceCodeResponse> { | ||
| const fqdn = await identityFqdn() | ||
| const identityClientId = clientId() | ||
| const scopes = allDefaultScopes() | ||
| const params = new URLSearchParams({client_id: identityClientId, scope: scopes.join(' ')}).toString() | ||
| const url = `https://${fqdn}/oauth/device_authorization` | ||
|
|
||
| const response = await shopifyFetch(url, { | ||
| method: 'POST', | ||
| headers: {'Content-type': 'application/x-www-form-urlencoded'}, | ||
| body: params, | ||
| }) | ||
|
|
||
| const responseText = await response.text() | ||
| let result: Record<string, unknown> | ||
| try { | ||
| result = JSON.parse(responseText) as Record<string, unknown> | ||
| } catch { | ||
| throw new BugError(`Invalid response from authorization service (HTTP ${response.status})`) | ||
| } | ||
|
|
||
| if (!result.device_code || !result.verification_uri_complete) { | ||
| throw new BugError('Failed to start device authorization') | ||
| } | ||
|
|
||
| return { | ||
| deviceCode: result.device_code as string, | ||
| userCode: result.user_code as string, | ||
| verificationUri: result.verification_uri as string, | ||
| verificationUriComplete: result.verification_uri_complete as string, | ||
| expiresIn: result.expires_in as number, | ||
| interval: (result.interval as number) ?? 5, | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Completes device authorization by polling for approval and exchanging tokens. | ||
| * | ||
| * @param deviceCode - The device code from requestDeviceCode. | ||
| * @param interval - Polling interval in seconds. | ||
| * @param storeFqdn - The normalized store FQDN. | ||
| * @returns An admin session with token and store FQDN. | ||
| */ | ||
| export async function completeDeviceAuth( | ||
| deviceCode: string, | ||
| interval: number, | ||
| storeFqdn: string, | ||
| ): Promise<AdminSession> { | ||
| const identityToken = await pollForDeviceAuthorization(deviceCode, interval) | ||
|
|
||
| const exchangeScopes: ExchangeScopes = { | ||
| admin: apiScopes('admin'), | ||
| partners: apiScopes('partners'), | ||
| storefront: apiScopes('storefront-renderer'), | ||
| businessPlatform: apiScopes('business-platform'), | ||
| appManagement: apiScopes('app-management'), | ||
| } | ||
|
|
||
| const appTokens = await exchangeAccessForApplicationTokens(identityToken, exchangeScopes, storeFqdn) | ||
|
|
||
| const fqdn = await identityFqdn() | ||
| const sessions = (await sessionStore.fetch()) ?? {} | ||
| const newSession = { | ||
| identity: identityToken, | ||
| applications: appTokens, | ||
| } | ||
|
|
||
| const updatedSessions = { | ||
| ...sessions, | ||
| [fqdn]: {...sessions[fqdn], [identityToken.userId]: newSession}, | ||
| } | ||
| await sessionStore.store(updatedSessions) | ||
| setCurrentSessionId(identityToken.userId) | ||
|
|
||
| const adminAppId = applicationId('admin') | ||
| const adminKey = `${storeFqdn}-${adminAppId}` | ||
| const adminToken = appTokens[adminKey] | ||
|
|
||
| if (!adminToken) { | ||
| throw new BugError(`No admin token received for store ${storeFqdn}`) | ||
| } | ||
|
|
||
| return {token: adminToken.accessToken, storeFqdn} | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| # @shopify/mcp | ||
|
|
||
| MCP server for the Shopify Admin API. Connects AI coding agents (Claude, Cursor, etc.) to your Shopify store via the [Model Context Protocol](https://modelcontextprotocol.io). | ||
|
|
||
| ## Setup | ||
|
|
||
| ```bash | ||
| claude mcp add shopify -- npx -y -p @shopify/mcp | ||
| ``` | ||
|
|
||
| Optionally set a default store so you don't have to pass it with every request: | ||
|
|
||
| ```bash | ||
| export SHOPIFY_FLAG_STORE=my-store.myshopify.com | ||
| ``` | ||
|
|
||
| ## Tools | ||
|
|
||
| ### `shopify_auth_login` | ||
|
|
||
| Authenticate with a Shopify store. Returns a URL the user must visit to complete login via device auth. After approval, subsequent `shopify_graphql` calls will use the session automatically. | ||
|
|
||
| | Parameter | Type | Required | Description | | ||
| |-----------|------|----------|-------------| | ||
| | `store` | string | No | Store domain. Defaults to `SHOPIFY_FLAG_STORE` env var. | | ||
|
|
||
| ### `shopify_graphql` | ||
|
|
||
| Execute a GraphQL query or mutation against the Shopify Admin API. Uses the latest supported API version. | ||
|
|
||
| | Parameter | Type | Required | Description | | ||
| |-----------|------|----------|-------------| | ||
| | `query` | string | Yes | GraphQL query or mutation string | | ||
| | `variables` | object | No | GraphQL variables | | ||
| | `store` | string | No | Store domain override. Defaults to `SHOPIFY_FLAG_STORE` env var. | | ||
| | `allowMutations` | boolean | No | Must be `true` to execute mutations. Safety measure to prevent unintended changes. | | ||
|
|
||
| ## Example | ||
|
|
||
| ``` | ||
| Agent: "List my products" | ||
|
|
||
| → shopify_auth_login(store: "my-store.myshopify.com") | ||
| ← "Open this URL to authenticate: https://accounts.shopify.com/activate?user_code=ABCD-EFGH" | ||
|
|
||
| [user approves in browser] | ||
|
|
||
| → shopify_graphql(query: "{ products(first: 5) { edges { node { title } } } }") | ||
| ← { "products": { "edges": [{ "node": { "title": "T-Shirt" } }, ...] } } | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| #!/usr/bin/env node | ||
| import '../dist/index.js' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| { | ||
| "name": "@shopify/mcp", | ||
| "version": "3.91.0", | ||
| "description": "MCP server for the Shopify Admin API", | ||
| "packageManager": "pnpm@10.11.1", | ||
| "private": false, | ||
| "keywords": [ | ||
| "shopify", | ||
| "mcp", | ||
| "model-context-protocol" | ||
| ], | ||
| "homepage": "https://github.com/shopify/cli#readme", | ||
| "bugs": { | ||
| "url": "https://community.shopify.dev/c/shopify-cli-libraries/14" | ||
| }, | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "https://github.com/Shopify/cli.git", | ||
| "directory": "packages/mcp" | ||
| }, | ||
| "license": "MIT", | ||
| "type": "module", | ||
| "bin": { | ||
| "shopify-mcp": "./bin/shopify-mcp.js" | ||
| }, | ||
| "files": [ | ||
| "/bin", | ||
| "/dist" | ||
| ], | ||
| "exports": { | ||
| ".": { | ||
| "import": "./dist/index.js", | ||
| "types": "./dist/index.d.ts" | ||
| } | ||
| }, | ||
| "scripts": { | ||
| "build": "nx build", | ||
| "clean": "nx clean", | ||
| "lint": "nx lint", | ||
| "lint:fix": "nx lint:fix", | ||
| "prepack": "NODE_ENV=production pnpm nx build && cp ../../README.md README.md", | ||
| "vitest": "vitest", | ||
| "type-check": "nx type-check" | ||
| }, | ||
| "eslintConfig": { | ||
| "extends": [ | ||
| "../../.eslintrc.cjs" | ||
| ] | ||
| }, | ||
| "dependencies": { | ||
| "@modelcontextprotocol/sdk": "1.22.0", | ||
| "@shopify/cli-kit": "3.91.0", | ||
| "zod": "3.24.1", | ||
| "zod-to-json-schema": "3.24.5" | ||
| }, | ||
| "devDependencies": { | ||
| "@vitest/coverage-istanbul": "^3.1.4" | ||
| }, | ||
| "engines": { | ||
| "node": ">=20.10.0" | ||
| }, | ||
| "os": [ | ||
| "darwin", | ||
| "linux", | ||
| "win32" | ||
| ], | ||
| "publishConfig": { | ||
| "@shopify:registry": "https://registry.npmjs.org", | ||
| "access": "public" | ||
| }, | ||
| "engine-strict": true | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| { | ||
| "name": "mcp", | ||
| "$schema": "../../node_modules/nx/schemas/project-schema.json", | ||
| "sourceRoot": "packages/mcp/src", | ||
| "projectType": "library", | ||
| "tags": ["scope:feature"], | ||
| "targets": { | ||
| "clean": { | ||
| "executor": "nx:run-commands", | ||
| "options": { | ||
| "command": "pnpm rimraf dist/", | ||
| "cwd": "packages/mcp" | ||
| } | ||
| }, | ||
| "build": { | ||
| "executor": "nx:run-commands", | ||
| "outputs": ["{workspaceRoot}/dist"], | ||
| "inputs": ["{projectRoot}/src/**/*", "{projectRoot}/package.json"], | ||
| "options": { | ||
| "command": "pnpm tsc -b ./tsconfig.build.json", | ||
| "cwd": "packages/mcp" | ||
| } | ||
| }, | ||
| "lint": { | ||
| "executor": "nx:run-commands", | ||
| "options": { | ||
| "command": "pnpm eslint \"src/**/*.ts\"", | ||
| "cwd": "packages/mcp" | ||
| } | ||
| }, | ||
| "lint:fix": { | ||
| "executor": "nx:run-commands", | ||
| "options": { | ||
| "command": "pnpm eslint 'src/**/*.ts' --fix", | ||
| "cwd": "packages/mcp" | ||
| } | ||
| }, | ||
| "type-check": { | ||
| "executor": "nx:run-commands", | ||
| "options": { | ||
| "command": "pnpm tsc --noEmit", | ||
| "cwd": "packages/mcp" | ||
| } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import {createServer} from './server.js' | ||
| import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js' | ||
|
|
||
| const server = createServer() | ||
| const transport = new StdioServerTransport() | ||
| await server.connect(transport) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unhandled startup failure can crash the MCP process (missing connect error handling)
|
||
|
|
||
| const shutdown = () => { | ||
| const _closing = server | ||
| .close() | ||
| .then(() => process.exit(0)) | ||
| .catch(() => process.exit(1)) | ||
| } | ||
| process.on('SIGINT', shutdown) | ||
| process.on('SIGTERM', shutdown) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import {SessionManager} from './session-manager.js' | ||
| import {registerAuthTool} from './tools/auth.js' | ||
| import {registerGraphqlTool} from './tools/graphql.js' | ||
| import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js' | ||
|
|
||
| import {createRequire} from 'module' | ||
|
|
||
| const require = createRequire(import.meta.url) | ||
| const {version} = require('../package.json') as {version: string} | ||
|
|
||
| export function createServer(): McpServer { | ||
| const server = new McpServer({ | ||
| name: 'shopify', | ||
| version, | ||
| }) | ||
|
|
||
| const sessionManager = new SessionManager() | ||
|
|
||
| registerAuthTool(server, sessionManager) | ||
| registerGraphqlTool(server, sessionManager) | ||
|
|
||
| return server | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.