create_variants
Add variants to an existing product by specifying option values for all product options, price, and optional initial inventory. Use REMOVE_STANDALONE_VARIANT strategy to replace the default placeholder variant.
Instructions
Create one or more variants on an existing product. Each variant's optionValues must cover EVERY option declared on the product (Size + Color + Material if there are 3 options) — partial coverage is rejected. New products from create_product start with a single hidden 'Default Title' variant; when adding the first real variants, pass strategy='REMOVE_STANDALONE_VARIANT' so Shopify replaces the placeholder rather than leaving it. inventoryQuantities seeds initial stock per location at create time; for ongoing changes use set_inventory_quantity instead.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| productId | Yes | Product GID. | |
| variants | Yes | ||
| strategy | No | DEFAULT: add to existing variants. REMOVE_STANDALONE_VARIANT: replace the auto-created 'Default Title' variant (use on first real variant create). |
Implementation Reference
- src/tools/variants.ts:198-207 (schema)Input schema for the create_variants tool — accepts productId, variants array (of variantCreateSchema), and an optional strategy enum.
const createVariantsSchema = { productId: z.string().describe("Product GID."), variants: z.array(variantCreateSchema).min(1).max(100), strategy: z .enum(["DEFAULT", "REMOVE_STANDALONE_VARIANT"]) .optional() .describe( "DEFAULT: add to existing variants. REMOVE_STANDALONE_VARIANT: replace the auto-created 'Default Title' variant (use on first real variant create).", ), }; - src/tools/variants.ts:157-180 (schema)variantCreateSchema: per-variant input definition including optionValues, price, compareAtPrice, sku, barcode, taxable, inventoryPolicy, and inventoryQuantities.
const variantCreateSchema = z.object({ optionValues: z .array(optionValueInputSchema) .describe( "One entry per product option. Shape must match the product's options (order-insensitive, matched by optionName).", ), price: z.string().describe("Variant price as decimal string, e.g. '19.99'."), compareAtPrice: z.string().optional(), sku: z.string().optional(), barcode: z.string().optional(), taxable: z.boolean().optional(), inventoryPolicy: z .enum(["DENY", "CONTINUE"]) .optional() .describe( "Oversell policy: DENY blocks sales at 0 stock, CONTINUE allows backorder.", ), inventoryQuantities: z .array(inventoryQuantityInputSchema) .optional() .describe( "Initial stock per location. Only accepted on create — use set_inventory_quantity for subsequent updates.", ), }); - src/tools/variants.ts:319-351 (handler)Handler for 'create_variants' tool — calls productVariantsBulkCreate GraphQL mutation, checks for user errors, and returns formatted results.
server.tool( "create_variants", "Create one or more variants on an existing product. Each variant's optionValues must cover EVERY option declared on the product (Size + Color + Material if there are 3 options) — partial coverage is rejected. New products from create_product start with a single hidden 'Default Title' variant; when adding the first real variants, pass strategy='REMOVE_STANDALONE_VARIANT' so Shopify replaces the placeholder rather than leaving it. inventoryQuantities seeds initial stock per location at create time; for ongoing changes use set_inventory_quantity instead.", createVariantsSchema, async (args) => { const data = await client.graphql<{ productVariantsBulkCreate: { productVariants: VariantNode[]; userErrors: ShopifyUserError[]; }; }>(VARIANTS_BULK_CREATE_MUTATION, { productId: args.productId, variants: args.variants, strategy: args.strategy, }); throwIfUserErrors( data.productVariantsBulkCreate.userErrors, "productVariantsBulkCreate", ); const created = data.productVariantsBulkCreate.productVariants; return { content: [ { type: "text" as const, text: [ `Created ${created.length} variant(s):`, ...created.map((v) => formatVariant(v)), ].join("\n"), }, ], }; }, ); - src/tools/variants.ts:262-351 (registration)Registration: function registerVariantTools registers the 'create_variants' tool on the McpServer (alongside other variant tools). The registration for create_variants specifically is at line 319.
export function registerVariantTools( server: McpServer, client: ShopifyClient, ): void { server.tool( "list_variants", "List all variants of a single product, plus the product's option definitions (Size, Color, etc.) and possible values. For each variant returns: title, GID, price, compareAtPrice, SKU, barcode, current inventory quantity, taxable flag, inventory policy, and the option-value combination that produced it. Use to inspect a product's full SKU matrix before calling create_variants/update_variants/delete_variants.", listVariantsSchema, async (args) => { const data = await client.graphql<{ product: | { id: string; title: string; options: ProductOptionNode[]; variants: { edges: Array<{ node: VariantNode }>; pageInfo: { hasNextPage: boolean }; }; } | null; }>(LIST_VARIANTS_QUERY, { productId: args.productId, first: args.first }); if (!data.product) { return { content: [ { type: "text" as const, text: `Product not found: ${args.productId}` }, ], }; } const p = data.product; const optionLines = p.options.map( (o) => ` ${o.name} (#${o.position}): ${o.optionValues.map((v) => v.name).join(", ")}`, ); const variantLines = p.variants.edges.map(({ node }) => formatVariant(node)); return { content: [ { type: "text" as const, text: [ `${p.title} — ${p.id}`, "Options:", ...optionLines, `Variants (${p.variants.edges.length}):`, ...variantLines, p.variants.pageInfo.hasNextPage ? "(more variants available; raise `first` to page further)" : "", ] .filter(Boolean) .join("\n"), }, ], }; }, ); server.tool( "create_variants", "Create one or more variants on an existing product. Each variant's optionValues must cover EVERY option declared on the product (Size + Color + Material if there are 3 options) — partial coverage is rejected. New products from create_product start with a single hidden 'Default Title' variant; when adding the first real variants, pass strategy='REMOVE_STANDALONE_VARIANT' so Shopify replaces the placeholder rather than leaving it. inventoryQuantities seeds initial stock per location at create time; for ongoing changes use set_inventory_quantity instead.", createVariantsSchema, async (args) => { const data = await client.graphql<{ productVariantsBulkCreate: { productVariants: VariantNode[]; userErrors: ShopifyUserError[]; }; }>(VARIANTS_BULK_CREATE_MUTATION, { productId: args.productId, variants: args.variants, strategy: args.strategy, }); throwIfUserErrors( data.productVariantsBulkCreate.userErrors, "productVariantsBulkCreate", ); const created = data.productVariantsBulkCreate.productVariants; return { content: [ { type: "text" as const, text: [ `Created ${created.length} variant(s):`, ...created.map((v) => formatVariant(v)), ].join("\n"), }, ], }; }, ); - src/tools/variants.ts:61-82 (helper)GraphQL mutation VARIANTS_BULK_CREATE_MUTATION used by the create_variants handler to bulk-create variants.
const VARIANTS_BULK_CREATE_MUTATION = /* GraphQL */ ` mutation VariantsBulkCreate( $productId: ID! $variants: [ProductVariantsBulkInput!]! $strategy: ProductVariantsBulkCreateStrategy ) { productVariantsBulkCreate( productId: $productId variants: $variants strategy: $strategy ) { productVariants { id title price sku selectedOptions { name value } } userErrors { field message } } } `;