Shopify MCP Server

export type Nullable<T> = T | null; export type ISODate = string; export type Maybe<T> = T | null | undefined; export type CreateDiscountCodeResponse = { id: string; priceRuleId: string; code: string; usageCount: number; }; export enum ShopifyWebhookTopicGraphql { ORDERS_UPDATED = "ORDERS_UPDATED", } export enum ShopifyWebhookTopic { ORDERS_UPDATED = "orders/updated", } export type ShopifyWebhook = { id: string; callbackUrl: string; topic: ShopifyWebhookTopic; }; export type ShopifyPriceRule = { id: number; value_type: string; value: string; customer_selection: string; target_type: string; target_selection: string; allocation_method: string; allocation_limit: number | null; once_per_customer: boolean; usage_limit: number | null; starts_at: string; ends_at: string | null; created_at: string; updated_at: string; entitled_product_ids: number[]; entitled_variant_ids: number[]; entitled_collection_ids: number[]; entitled_country_ids: number[]; prerequisite_product_ids: number[]; prerequisite_variant_ids: number[]; prerequisite_collection_ids: number[]; prerequisite_saved_search_ids: number[]; prerequisite_customer_ids: number[]; prerequisite_subtotal_range: { greater_than_or_equal_to: string; } | null; prerequisite_quantity_range: { greater_than_or_equal_to: number; } | null; prerequisite_shipping_price_range: { less_than_or_equal_to: string; } | null; prerequisite_to_entitlement_quantity_ratio: { prerequisite_quantity: number; entitled_quantity: number; } | null; title: string; admin_graphql_api_id: string; }; export type ShopifyCreatePriceRuleResponse = { price_rule: ShopifyPriceRule; }; export type ShopifyDiscountCode = { id: number; price_rule_id: number; code: string; usage_count: number; created_at: string; updated_at: string; }; export type ShopifyCreateDiscountCodeResponse = { discount_code: ShopifyDiscountCode; }; export type CreatePriceRuleInput = { title: string; targetType: "LINE_ITEM" | "SHIPPING_LINE"; allocationMethod: "ACROSS" | "EACH"; valueType: "fixed_amount" | "percentage"; value: string; entitledCollectionIds: string[]; usageLimit?: number; startsAt: ISODate; endsAt?: ISODate; }; export type CreateBasicDiscountCodeInput = { title: string; code: string; startsAt: ISODate; endsAt?: ISODate; valueType: string; value: number; usageLimit?: number; includeCollectionIds: string[]; excludeCollectionIds: string[]; appliesOncePerCustomer: boolean; combinesWith: { productDiscounts: boolean; orderDiscounts: boolean; shippingDiscounts: boolean; }; }; export type CreateBasicDiscountCodeResponse = { id: string; code: string; }; export type BasicDiscountCodeResponse = { data: { discountCodeBasicCreate: { codeDiscountNode: { id: string; codeDiscount: { title: string; codes: { nodes: Array<{ code: string; }>; }; startsAt: string; endsAt: string; customerSelection: { allCustomers: boolean; }; customerGets: { appliesOnOneTimePurchase: boolean; appliesOnSubscription: boolean; value: { percentage?: number; amount?: { amount: number; currencyCode: string; }; }; items: { allItems: boolean; }; }; appliesOncePerCustomer: boolean; recurringCycleLimit: number; }; }; userErrors: Array<{ field: string[]; code: string; message: string; }>; }; }; }; export type CreatePriceRuleResponse = { id: string; }; type DiscountCode = { code: string | null; amount: string | null; type: string | null; }; export type ShopifyCustomer = { id?: number; email?: string; first_name?: string; last_name?: string; phone?: string; orders_count?: number; email_marketing_consent?: { state?: "subscribed" | "not_subscribed" | null; opt_in_level?: "single_opt_in" | "confirmed_opt_in" | "unknown" | null; consent_updated_at?: string; }; sms_marketing_consent?: { state?: string; opt_in_level?: string | null; consent_updated_at?: string; consent_collected_from?: string; }; tags?: string; currency?: string; default_address?: { first_name?: string | null; last_name?: string | null; company?: string | null; address1?: string | null; address2?: string | null; city?: string | null; province?: string | null; country?: string | null; zip?: string | null; phone?: string | null; name?: string | null; province_code?: string | null; country_code?: string | null; country_name?: string | null; }; }; export type LoadCustomersResponse = { customers: Array<ShopifyCustomer>; next?: string | undefined; }; export type ShopifyOrder = { id: string; createdAt: string; currencyCode: string; discountApplications: { nodes: Array<{ code: string | null; value: { amount: string | null; percentage: number | null; }; __typename: string; }>; }; displayFinancialStatus: string | null; name: string; totalPriceSet: { shopMoney: { amount: string; currencyCode: string }; presentmentMoney: { amount: string; currencyCode: string }; }; totalShippingPriceSet: { shopMoney: { amount: string; currencyCode: string }; presentmentMoney: { amount: string; currencyCode: string }; }; customer?: { id: string; email: string; firstName: string; lastName: string; phone: string; }; }; export type ShopifyOrdersResponse = { data: { orders: { edges: Array<{ node: ShopifyOrder; }>; pageInfo: { hasNextPage: boolean; endCursor: string; }; }; }; }; export function isShopifyOrder( shopifyOrder: any ): shopifyOrder is ShopifyOrder { return ( shopifyOrder && "id" in shopifyOrder && "createdAt" in shopifyOrder && "currencyCode" in shopifyOrder && "discountApplications" in shopifyOrder && "displayFinancialStatus" in shopifyOrder && "name" in shopifyOrder && "totalPriceSet" in shopifyOrder && "totalShippingPriceSet" in shopifyOrder ); } // Shopify webhook payload is the same type as the order // We expose the same type for having an easier to read and consistent API across all webshop clients export type ShopifyOrderWebhookPayload = ShopifyOrder; export function isShopifyOrderWebhookPayload( webhookPayload: any ): webhookPayload is ShopifyOrderWebhookPayload { return isShopifyOrder(webhookPayload); } export type ShopifyCollectionsQueryParams = { sinceId?: string; // Retrieve all orders after the specified ID name?: string; limit: number; }; export type ShopifyCollection = { id: number; handle: string; title: string; updated_at: string; body_html: Nullable<string>; published_at: string; sort_order: string; template_suffix?: Nullable<string>; published_scope: string; image?: { src: string; alt: string; }; }; export type ShopifySmartCollectionsResponse = { smart_collections: ShopifyCollection[]; }; export type ShopifyCustomCollectionsResponse = { custom_collections: ShopifyCollection[]; }; export type LoadCollectionsResponse = { collections: ShopifyCollection[]; next?: string; }; export type ShopifyShop = { id: string; name: string; domain: string; myshopify_domain: string; currency: string; enabled_presentment_currencies: string[]; address1: string; created_at: string; updated_at: string; }; export type LoadStorefrontsResponse = { shop: ShopifyShop; }; export type ShopifyQueryParams = { query?: string; // Custom query string for advanced filtering sortKey?: | "PROCESSED_AT" | "TOTAL_PRICE" | "ID" | "CREATED_AT" | "UPDATED_AT" | "ORDER_NUMBER"; reverse?: boolean; before?: string; after?: string; // Keeping these for backwards compatibility, but they should be used in query string sinceId?: string; updatedAtMin?: string; createdAtMin?: string; financialStatus?: | "AUTHORIZED" | "PENDING" | "PAID" | "PARTIALLY_PAID" | "REFUNDED" | "VOIDED" | "PARTIALLY_REFUNDED" | "ANY" | "UNPAID"; ids?: string[]; status?: "OPEN" | "CLOSED" | "CANCELLED" | "ANY"; limit?: number; }; export type ShippingZone = { id: string; name: string; countries: Array<{ id: string; name: string; code: string; }>; }; export type ShopifyLoadOrderQueryParams = { orderId: string; fields?: string[]; }; export type ProductImage = { src: string; height: number; width: number; }; export type ProductOption = { id: string; name: string; values: string[]; }; export type SelectedProductOption = { name: string; value: string; }; export type ProductVariant = { id: string; title: string; price: string; sku: string; availableForSale: boolean; image: Nullable<ProductImage>; inventoryPolicy: "CONTINUE" | "DENY"; selectedOptions: SelectedProductOption[]; }; export type ShopResponse = { data: { shop: { shipsToCountries: string[]; }; }; }; export type MarketResponse = { data: { market: { name: string; enabled: string; regions: { nodes: { name: string; code: string; }; }; }; }; }; export type GetPriceRuleInput = { query?: string }; export type GetPriceRuleResponse = { priceRules: { nodes: [ { id: string; title: string; status: string; } ]; }; }; export type ProductVariantWithProductDetails = ProductVariant & { product: { id: string; title: string; description: string; images: { edges: { node: ProductImage; }[]; }; }; }; export type ProductNode = { id: string; handle: string; title: string; description: string; publishedAt: string; updatedAt: string; options: ProductOption[]; images: { edges: { node: ProductImage; }[]; }; variants: { edges: { node: ProductVariant; }[]; }; }; export type LoadProductsResponse = { currencyCode: string; products: ProductNode[]; next?: string; }; export type LoadProductsByIdsResponse = { currencyCode: string; products: ProductNode[]; }; export type LoadVariantsByIdResponse = { currencyCode: string; variants: ProductVariantWithProductDetails[]; }; export type CreateDraftOrderPayload = { lineItems: Array<{ variantId: string; quantity: number; appliedDiscount?: { title: string; value: number; valueType: "FIXED_AMOUNT" | "PERCENTAGE"; }; }>; shippingAddress: { address1: string; address2?: string; countryCode: string; firstName: string; lastName: string; zip: string; city: string; country: string; province?: string; provinceCode?: string; phone?: string; }; billingAddress: { address1: string; address2?: string; countryCode: string; firstName: string; lastName: string; zip: string; city: string; country: string; province?: string; provinceCode?: string; phone?: string; }; email: string; tags: string; note: string; }; export type DraftOrderResponse = { draftOrderId: string; draftOrderName: string; }; export type CompleteDraftOrderResponse = { draftOrderId: string; draftOrderName: string; orderId: string; }; function serializeError(err: any): any { if (Array.isArray(err)) { return err.map((item) => serializeError(item)); } else if (typeof err === "object" && err !== null) { const result: Record<string, any> = {}; Object.getOwnPropertyNames(err).forEach((key) => { result[key] = serializeError(err[key]); }); return result; } return err; } type InnerError = | Error | Error[] | string | string[] | Record<string, any> | undefined; export interface CustomErrorPayload { customCode?: string; message?: string; innerError?: InnerError; /** * Used to add custom data that will be logged */ contextData?: any; } export class CustomError extends Error { public code: string; public innerError: InnerError; public contextData: any; constructor(message: string, code: string, payload: CustomErrorPayload = {}) { super(message); this.code = payload.customCode ? `${code}.${payload.customCode}` : code; if (payload.message) this.message = message; this.innerError = payload.innerError; this.contextData = payload.contextData; this.name = this.constructor.name; } toJSON(): unknown { return { message: this.message, innerError: serializeError(this.innerError), name: this.name, code: this.code, contextData: this.contextData, }; } static is<E extends typeof CustomError & { code: string }>( error: any, ErrorClass: E ): error is InstanceType<E> { return "code" in error && error.code === ErrorClass.code; } } export class ShopifyClientErrorBase extends CustomError { // eslint-disable-next-line @typescript-eslint/explicit-function-return-type static make(message: string, code: string) { return class extends ShopifyClientErrorBase { static code = code; constructor(payload?: CustomErrorPayload) { super(message, code, payload); } }; } } export class ShopifyCastObjError extends ShopifyClientErrorBase.make( "Error occurred on Shopify cast object", "SHOPIFY_CLIENT.SHOPIFY_CAST_ERROR" ) {} export class ShopifyAuthorizationError extends ShopifyClientErrorBase.make( "Shopify authorization error", "SHOPIFY_CLIENT.AUTHORIZATION_ERROR" ) {} export class ShopifyRequestError extends ShopifyClientErrorBase.make( "Shopify request error", "SHOPIFY_CLIENT.REQUEST_ERROR" ) {} export class ShopifyInputError extends ShopifyClientErrorBase.make( "Shopify input error", "SHOPIFY_CLIENT.INPUT_ERROR" ) {} export class ShopifyRateLimitingError extends ShopifyClientErrorBase.make( "Shopify rate limiting error", "SHOPIFY_CLIENT.RATE_LIMITING_ERROR" ) {} export class ShopifyServerInfrastructureError extends ShopifyClientErrorBase.make( "Shopify server or infrastructure error", "SHOPIFY_CLIENT.SERVER_INFRASTRUCTURE_ERROR" ) {} export class ShopifyPaymentError extends ShopifyClientErrorBase.make( "Shopify payment error", "SHOPIFY_CLIENT.PAYMENT_ERROR" ) {} export class GeneralShopifyClientError extends ShopifyClientErrorBase.make( "Error occurred on Shopify API client", "SHOPIFY_CLIENT.SHOPIFY_CLIENT_ERROR" ) {} export class ShopifyWebShopNotFoundError extends ShopifyClientErrorBase.make( "The Shopify webshop not found", "SHOPIFY_CLIENT.WEBSHOP_CONNECTION_NOT_FOUND" ) {} export class ShopifyProductVariantNotFoundError extends ShopifyClientErrorBase.make( "The Shopify product variant not found", "SHOPIFY_CLIENT.PRODUCT_VARIANT_NOT_FOUND" ) {} export class ShopifyProductVariantNotAvailableForSaleError extends ShopifyClientErrorBase.make( "The Shopify product variant is not available for sale", "SHOPIFY_CLIENT.PRODUCT_VARIANT_NOT_AVAILABLE_FOR_SALE" ) {} export class InvalidShopifyCurrencyError extends ShopifyClientErrorBase.make( "The Shopify currency is invalid", "SHOPIFY_CLIENT.INVALID_CURRENCY" ) {} export class ShopifyWebhookNotFoundError extends ShopifyClientErrorBase.make( "The Shopify webhook not found", "SHOPIFY_CLIENT.WEBHOOK_NOT_FOUND" ) {} export class ShopifyWebhookAlreadyExistsError extends ShopifyClientErrorBase.make( "The Shopify webhook already exists", "SHOPIFY_CLIENT.WEBHOOK_ALREADY_EXISTS" ) {} export function getHttpShopifyError( error: any, statusCode: number, contextData?: Record<string, any> ): ShopifyClientErrorBase { switch (statusCode) { case 401: case 403: case 423: case 430: return new ShopifyAuthorizationError({ innerError: error, contextData }); case 400: case 405: case 406: case 414: case 415: case 783: return new ShopifyRequestError({ innerError: error, contextData }); case 404: case 409: case 422: return new ShopifyInputError({ innerError: error, contextData }); case 429: return new ShopifyRateLimitingError({ innerError: error, contextData }); case 500: case 501: case 502: case 503: case 504: case 530: case 540: return new ShopifyServerInfrastructureError({ innerError: error, contextData, }); case 402: return new ShopifyPaymentError({ innerError: error, contextData }); default: return new GeneralShopifyClientError({ innerError: error, contextData, }); } } export function getGraphqlShopifyUserError( errors: any[], contextData?: Record<string, any> ): ShopifyClientErrorBase { const hasErrorWithMessage = (messages: string[]): boolean => errors.some((error) => messages.includes(error.message)); if (hasErrorWithMessage(["Product variant not found."])) { return new ShopifyProductVariantNotFoundError({ innerError: errors, contextData, }); } if (hasErrorWithMessage(["Webhook subscription does not exist"])) { return new ShopifyWebhookNotFoundError({ innerError: errors, contextData, }); } if (hasErrorWithMessage(["Address for this topic has already been taken"])) { return new ShopifyWebhookAlreadyExistsError({ innerError: errors, contextData, }); } return new GeneralShopifyClientError({ innerError: errors, contextData, }); } export function getGraphqlShopifyError( errors: any[], statusCode: number, contextData?: Record<string, any> ): ShopifyClientErrorBase { const hasErrorWithCode = (codes: string[]): boolean => errors.some((error) => codes.includes(error.extensions?.code)); switch (statusCode) { case 403: case 423: return new ShopifyAuthorizationError({ innerError: errors, contextData, }); case 400: return new ShopifyRequestError({ innerError: errors, contextData, }); case 404: return new ShopifyInputError({ innerError: errors, contextData, }); case 500: case 501: case 502: case 503: case 504: case 530: case 540: return new ShopifyServerInfrastructureError({ innerError: errors, contextData, }); case 402: return new ShopifyPaymentError({ innerError: errors, contextData, }); default: if (hasErrorWithCode(["UNAUTHORIZED", "ACCESS_DENIED", "FORBIDDEN"])) { return new ShopifyAuthorizationError({ innerError: errors, contextData, }); } if (hasErrorWithCode(["UNPROCESSABLE"])) { return new ShopifyInputError({ innerError: errors, contextData, }); } if (hasErrorWithCode(["THROTTLED"])) { return new ShopifyRateLimitingError({ innerError: errors, contextData, }); } if (hasErrorWithCode(["INTERNAL_SERVER_ERROR"])) { return new ShopifyServerInfrastructureError({ innerError: errors, contextData, }); } return new GeneralShopifyClientError({ innerError: errors, contextData, }); } } export type ShopifyOrderGraphql = { id: string; name: string; createdAt: string; displayFinancialStatus: string; email: string; phone: string | null; totalPriceSet: { shopMoney: { amount: string; currencyCode: string }; presentmentMoney: { amount: string; currencyCode: string }; }; customer: { id: string; email: string; } | null; shippingAddress: { provinceCode: string | null; countryCode: string; } | null; lineItems: { nodes: Array<{ id: string; title: string; quantity: number; originalTotalSet: { shopMoney: { amount: string; currencyCode: string }; }; variant: { id: string; title: string; sku: string | null; price: string; } | null; }>; }; }; export type ShopifyOrdersGraphqlQueryParams = { first?: number; after?: string; query?: string; sortKey?: | "PROCESSED_AT" | "TOTAL_PRICE" | "ID" | "CREATED_AT" | "UPDATED_AT" | "ORDER_NUMBER"; reverse?: boolean; }; export type ShopifyOrdersGraphqlResponse = { orders: ShopifyOrderGraphql[]; pageInfo: { hasNextPage: boolean; endCursor: string | null; }; }; export interface ShopifyClientPort { createPriceRule( accessToken: string, shop: string, priceRuleInput: CreatePriceRuleInput ): Promise<CreatePriceRuleResponse>; createDiscountCode( accessToken: string, shop: string, code: string, priceRuleId: string ): Promise<CreateDiscountCodeResponse>; deletePriceRule( accessToken: string, shop: string, priceRuleId: string ): Promise<void>; deleteDiscountCode( accessToken: string, shop: string, priceRuleId: string, discountCodeId: string ): Promise<void>; createBasicDiscountCode( accessToken: string, shop: string, discountInput: CreateBasicDiscountCodeInput ): Promise<CreateBasicDiscountCodeResponse>; deleteBasicDiscountCode( accessToken: string, shop: string, discountCodeId: string ): Promise<void>; loadOrders( accessToken: string, shop: string, queryParams: ShopifyOrdersGraphqlQueryParams ): Promise<ShopifyOrdersGraphqlResponse>; loadOrder( accessToken: string, myshopifyDomain: string, queryParams: ShopifyLoadOrderQueryParams ): Promise<ShopifyOrder>; subscribeWebhook( accessToken: string, myshopifyDomain: string, callbackUrl: string, topic: ShopifyWebhookTopic ): Promise<ShopifyWebhook>; unsubscribeWebhook( accessToken: string, myshopifyDomain: string, webhookId: string ): Promise<void>; findWebhookByTopicAndCallbackUrl( accessToken: string, myshopifyDomain: string, callbackUrl: string, topic: ShopifyWebhookTopic ): Promise<ShopifyWebhook | null>; loadCollections( accessToken: string, myshopifyDomain: string, queryParams: ShopifyQueryParams, next?: string ): Promise<LoadCollectionsResponse>; loadShop( accessToken: string, myshopifyDomain: string ): Promise<LoadStorefrontsResponse>; loadCustomers( accessToken: string, myshopifyDomain: string, limit?: number, next?: string ): Promise<LoadCustomersResponse>; tagCustomer( accessToken: string, myshopifyDomain: string, tags: string[], customerId: string ): Promise<boolean>; loadProducts( accessToken: string, myshopifyDomain: string, searchTitle: string | null, limit?: number, afterCursor?: string ): Promise<LoadProductsResponse>; loadProductsByCollectionId( accessToken: string, myshopifyDomain: string, collectionId: string, limit?: number, afterCursor?: string ): Promise<LoadProductsResponse>; loadProductsByIds( accessToken: string, shop: string, productIds: string[] ): Promise<LoadProductsByIdsResponse>; loadVariantsByIds( accessToken: string, shop: string, variantIds: string[] ): Promise<LoadVariantsByIdResponse>; createDraftOrder( accessToken: string, shop: string, draftOrderData: CreateDraftOrderPayload, idempotencyKey: string ): Promise<DraftOrderResponse>; completeDraftOrder( accessToken: string, shop: string, draftOrderId: string, variantId: string ): Promise<CompleteDraftOrderResponse>; getIdFromGid(gid: string): string; loadShopDetail(accessToken: string, shop: string): Promise<ShopResponse>; }