create_product
Creates a new product with title and optional details, defaulting to DRAFT status. After creation, attaches provided image URLs. Returns product GID and handle.
Instructions
Create a new product. The product is created first, then any image_urls (publicly fetchable) are attached as a follow-up call — Shopify pulls each URL and hosts the image on its CDN. The default status is DRAFT to prevent accidentally publishing half-configured products to the storefront; pass status=ACTIVE only when you're ready to go live. New products start with a single hidden 'Default Title' variant; to add real variants with options, call create_variants with strategy='REMOVE_STANDALONE_VARIANT'. Returns the new product's GID and handle.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| title | Yes | ||
| description | No | Description as HTML | |
| vendor | No | ||
| product_type | No | ||
| tags | No | ||
| status | No | DRAFT | |
| image_urls | No | Image URLs to attach after creation |
Implementation Reference
- src/tools/products.ts:213-251 (handler)The tool handler function for 'create_product'. It calls the Shopify GraphQL productCreate mutation with the input (title, descriptionHtml, vendor, productType, tags, status), then optionally attaches image URLs via attachImages, and returns a text result with the product GID and handle.
server.tool( "create_product", "Create a new product. The product is created first, then any image_urls (publicly fetchable) are attached as a follow-up call — Shopify pulls each URL and hosts the image on its CDN. The default `status` is DRAFT to prevent accidentally publishing half-configured products to the storefront; pass status=ACTIVE only when you're ready to go live. New products start with a single hidden 'Default Title' variant; to add real variants with options, call create_variants with strategy='REMOVE_STANDALONE_VARIANT'. Returns the new product's GID and handle.", createProductSchema, async (args) => { const data = await client.graphql<{ productCreate: { product: Product | null; userErrors: ShopifyUserError[]; }; }>(CREATE_PRODUCT_MUTATION, { input: { title: args.title, descriptionHtml: args.description, vendor: args.vendor, productType: args.product_type, tags: args.tags, status: args.status, }, }); throwIfUserErrors(data.productCreate.userErrors, "productCreate"); const product = data.productCreate.product; if (!product) throw new Error("productCreate returned no product"); const attached: string[] = []; if (args.image_urls?.length) { await attachImages(client, product.id, args.image_urls); attached.push(...args.image_urls); } const lines = [ `Created ${args.status} product: ${product.title} (${product.id})`, ` handle: ${product.handle}`, ]; if (attached.length) { lines.push(` attached ${attached.length} image(s)`); } return { content: [{ type: "text" as const, text: lines.join("\n") }] }; }, - src/tools/products.ts:129-140 (schema)The input schema for create_product: title (required), description, vendor, product_type, tags, status (defaults to DRAFT), and image_urls (array of URL strings).
const createProductSchema = { title: z.string().min(1), description: z.string().optional().describe("Description as HTML"), vendor: z.string().optional(), product_type: z.string().optional(), tags: z.array(z.string()).optional(), status: z.enum(["ACTIVE", "DRAFT", "ARCHIVED"]).default("DRAFT"), image_urls: z .array(z.string().url()) .optional() .describe("Image URLs to attach after creation"), }; - src/tools/products.ts:213-252 (registration)Registration: the tool is registered via server.tool('create_product', ...) inside the registerProductTools function. This function is called from src/server.ts:57.
server.tool( "create_product", "Create a new product. The product is created first, then any image_urls (publicly fetchable) are attached as a follow-up call — Shopify pulls each URL and hosts the image on its CDN. The default `status` is DRAFT to prevent accidentally publishing half-configured products to the storefront; pass status=ACTIVE only when you're ready to go live. New products start with a single hidden 'Default Title' variant; to add real variants with options, call create_variants with strategy='REMOVE_STANDALONE_VARIANT'. Returns the new product's GID and handle.", createProductSchema, async (args) => { const data = await client.graphql<{ productCreate: { product: Product | null; userErrors: ShopifyUserError[]; }; }>(CREATE_PRODUCT_MUTATION, { input: { title: args.title, descriptionHtml: args.description, vendor: args.vendor, productType: args.product_type, tags: args.tags, status: args.status, }, }); throwIfUserErrors(data.productCreate.userErrors, "productCreate"); const product = data.productCreate.product; if (!product) throw new Error("productCreate returned no product"); const attached: string[] = []; if (args.image_urls?.length) { await attachImages(client, product.id, args.image_urls); attached.push(...args.image_urls); } const lines = [ `Created ${args.status} product: ${product.title} (${product.id})`, ` handle: ${product.handle}`, ]; if (attached.length) { lines.push(` attached ${attached.length} image(s)`); } return { content: [{ type: "text" as const, text: lines.join("\n") }] }; }, ); - src/tools/products.ts:70-82 (helper)The GraphQL mutation string CREATE_PRODUCT_MUTATION used by the create_product handler.
const CREATE_PRODUCT_MUTATION = /* GraphQL */ ` mutation CreateProduct($input: ProductInput!) { productCreate(input: $input) { product { id title handle status } userErrors { field message } } } `; - src/tools/products.ts:308-330 (helper)The attachImages helper function used by create_product to attach image URLs to a product after creation.
export async function attachImages( client: ShopifyClient, productId: string, imageUrls: string[], altText?: string, ): Promise<Array<{ id: string }>> { const data = await client.graphql<{ productCreateMedia: { media: Array<{ id: string } | null>; mediaUserErrors: ShopifyUserError[]; }; }>(CREATE_MEDIA_MUTATION, { productId, media: imageUrls.map((url) => ({ originalSource: url, mediaContentType: "IMAGE", alt: altText, })), }); throwIfUserErrors(data.productCreateMedia.mediaUserErrors, "productCreateMedia"); return data.productCreateMedia.media.filter( (m): m is { id: string } => m !== null, );