upload_product_image
Attach a product image from a public URL to an existing Shopify product. Each call adds a new image without removing existing ones.
Instructions
Attach an image to an existing product by URL. Shopify fetches the URL server-side and hosts the file on its CDN — the URL must be publicly reachable from Shopify's network. Multiple calls add multiple images; this tool does not replace existing images. Use the bridge tools (generate_product_image, refine_product_image) instead when you want the image generated by ComfyUI rather than provided as a URL.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| product_id | Yes | Product GID or numeric ID | |
| image_url | Yes | Public image URL to attach | |
| alt_text | No |
Implementation Reference
- src/tools/products.ts:289-305 (handler)The tool handler function for 'upload_product_image'. It converts the product_id to a GID, then calls attachImages to attach the image URL to the product via Shopify's GraphQL API.
server.tool( "upload_product_image", "Attach an image to an existing product by URL. Shopify fetches the URL server-side and hosts the file on its CDN — the URL must be publicly reachable from Shopify's network. Multiple calls add multiple images; this tool does not replace existing images. Use the bridge tools (generate_product_image, refine_product_image) instead when you want the image generated by ComfyUI rather than provided as a URL.", uploadProductImageSchema, async (args) => { const productId = toGid(args.product_id, "Product"); const result = await attachImages(client, productId, [args.image_url], args.alt_text); return { content: [ { type: "text" as const, text: `Attached image to ${productId}: ${result.map((m) => m.id).join(", ")}`, }, ], }; }, ); - src/tools/products.ts:152-156 (schema)Input schema for upload_product_image: product_id (string), image_url (URL string), and optional alt_text.
const uploadProductImageSchema = { product_id: z.string().describe("Product GID or numeric ID"), image_url: z.string().url().describe("Public image URL to attach"), alt_text: z.string().optional(), }; - src/tools/products.ts:289-305 (registration)Registration of 'upload_product_image' via server.tool() inside registerProductTools.
server.tool( "upload_product_image", "Attach an image to an existing product by URL. Shopify fetches the URL server-side and hosts the file on its CDN — the URL must be publicly reachable from Shopify's network. Multiple calls add multiple images; this tool does not replace existing images. Use the bridge tools (generate_product_image, refine_product_image) instead when you want the image generated by ComfyUI rather than provided as a URL.", uploadProductImageSchema, async (args) => { const productId = toGid(args.product_id, "Product"); const result = await attachImages(client, productId, [args.image_url], args.alt_text); return { content: [ { type: "text" as const, text: `Attached image to ${productId}: ${result.map((m) => m.id).join(", ")}`, }, ], }; }, ); - src/tools/products.ts:308-331 (helper)The attachImages helper function that calls the productCreateMedia GraphQL mutation to attach images to a product by URL.
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, ); } - src/shopify/upload.ts:30-79 (helper)The stagedUploadImage helper for uploading raw image bytes to Shopify's staged storage (used by bridge tools, not upload_product_image directly).
export async function stagedUploadImage( client: ShopifyClient, bytes: Uint8Array, filename: string, mimeType: string, ): Promise<string> { const data = await client.graphql<{ stagedUploadsCreate: { stagedTargets: StagedTarget[]; userErrors: ShopifyUserError[]; }; }>(STAGED_UPLOADS_CREATE_MUTATION, { input: [ { resource: "IMAGE", filename, mimeType, fileSize: String(bytes.length), httpMethod: "POST", }, ], }); throwIfUserErrors( data.stagedUploadsCreate.userErrors, "stagedUploadsCreate", ); const target = data.stagedUploadsCreate.stagedTargets[0]; if (!target) { throw new Error("stagedUploadsCreate returned no target"); } const form = new FormData(); for (const { name, value } of target.parameters) { form.append(name, value); } form.append("file", new Blob([new Uint8Array(bytes)], { type: mimeType }), filename); const uploadRes = await fetch(target.url, { method: "POST", body: form, }); if (!uploadRes.ok && uploadRes.status !== 201 && uploadRes.status !== 204) { throw new Error( `Shopify staged upload POST failed: ${uploadRes.status} ${await uploadRes.text()}`, ); } return target.resourceUrl; }