--- title: Discover products with Shopify Catalog description: >- Learn how to search for products across the global Shopify Catalog and select a variant for checkout. source_url: html: 'https://shopify.dev/docs/agents/get-started/search-catalog' md: 'https://shopify.dev/docs/agents/get-started/search-catalog.md' --- # Discover products with Shopify Catalog This guide is the third part of a four-part tutorial series that describes how to build an agentic commerce application with the Universal Commerce Protocol (UCP) using Shopify's MCP servers. It demonstrates how to create a custom catalog, search for products using natural language queries, and walk a buyer through selecting a product variant. By the end of this tutorial, you'll have extended the demo scripts from the [Profile](https://shopify.dev/docs/agents/get-started/profile) tutorial to search the Catalog, display results, and walk a buyer through selecting a product variant. **Catalog capability in UCP:** Shopify's Catalog MCP is evolving toward the UCP spec [Catalog capability](https://ucp.dev/latest/specification/catalog/) and [MCP binding](https://ucp.dev/latest/specification/catalog/mcp/). Tool names, request and response shapes differ, so follow this documentation to build with Catalog on Shopify. *** ## What you'll learn In this tutorial, you'll learn to: * Create a custom catalog in the Dev Dashboard * Search for products using natural language queries * Apply filters to refine results * Retrieve product details and let a buyer select a variant *** ## Requirements * Complete the [Authenticate your agent](https://shopify.dev/docs/agents/get-started/authentication) and [Profile](https://shopify.dev/docs/agents/get-started/profile) tutorials *** ## Step 1: Create a custom catalog Catalogs define the scope of products your agent can discover. That scope can be across all of Shopify platform, or a filtered subset you define. 1. In [Dev Dashboard](https://dev.shopify.com/dashboard/) click **Catalogs** from the sidebar. 2. Click **Create a catalog**. 3. Keep the defaults, which place no bounds on price and search across all of Shopify's products. ![Dev Dashboard catalog configuration with filter options](https://shopify.dev/assets/assets/images/agents/catalog-new-DcNg4hGB.png) 4. Click **Save catalog**. 5. On the **Catalogs** landing page, click **Copy URL**. *** ## Step 2: Set up search Create a `search.js` file. Paste the URL you copied in the previous step to the `CATALOG_URL` variable. The unique `CATALOG_ID` will be pulled from that URL for subsequent searches. ## search.js ```javascript const CATALOG_URL = '{your_catalog_url}'; const CATALOG_ID = CATALOG_URL.split('/search/')[1]; export function showCatalog() { console.log('\n── 2. Search the Catalog ─────────────────────────\n'); console.log(` Catalog ID: ${CATALOG_ID}\n`); } ``` Update `ucp_demo.js` to import and call `showCatalog()`: ## ucp\_demo.js ```javascript import { getAccessToken } from './auth.js'; import { showCatalog } from './search.js'; async function main() { // 1. Authentication const token = await getAccessToken(); // 2. Search the Catalog showCatalog(); } main().catch(err => console.error('Request failed:', err)); ``` Run `node ucp_demo.js` again to see the changes: ## Output ── 1. Authentication ───────────────────────── Scopes: read\_global\_api\_catalog\_search Expires: 6:02:46 PM ── 2. Search the Catalog ───────────────────────── Catalog ID: {your\_catalog\_id} *** ## Step 3: Create the prompt utility Create a `utils.js` file, which defines a small helper that wraps Node's `readline` interface to handle interactive prompts throughout the tutorial. ## utils.js ```javascript import readline from 'readline'; export function prompt(question) { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); return new Promise(resolve => rl.question(question, answer => { rl.close(); resolve(answer); })); } ``` *** ## Step 4: Search for products Add a `searchProducts` function to the `search.js` file. The function accepts a `query` pulled from the prompt utility, then passes it to a call to the [`search_global_products`](https://shopify.dev/docs/agents/catalog/mcp#search_global_products) tool: ##### search.js ```javascript import { prompt } from './utils.js'; const CATALOG_URL = '{your_catalog_url}'; const CATALOG_ID = CATALOG_URL.split('/search/')[1]; export function showCatalog() { console.log('\n── 2. Search the Catalog ─────────────────────────\n'); console.log(` Catalog ID: ${CATALOG_ID}\n`); } export function displayOffers(offers) { console.log('\n── Results ────────────────────────────────────────\n'); offers.forEach((offer, i) => { const price = `$${(offer.priceRange.min.amount / 100).toFixed(2)}`; const options = offer.options?.map(o => `${o.name}: ${o.values.map(v => v.value).join(', ')}`).join(' | ') ?? '—'; console.log(` [${i + 1}] ${offer.title} | ${price} | ${options}`); }); console.log(); } export async function searchProducts(token, options = {}) { const query = process.argv[2] || await prompt('\x1b[1m Hello! What are you looking for today?\x1b[0m\n\n > '); const res = await fetch('https://discover.shopifyapps.com/global/mcp', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ jsonrpc: '2.0', method: 'tools/call', id: 1, params: { name: 'search_global_products', arguments: { saved_catalog: CATALOG_ID, query, context: '', limit: 10, ...options } } }) }); const data = await res.json(); if (!data.result?.content?.[0]) return null; return JSON.parse(data.result.content[0].text); } ``` ##### {} MCP input reference ```json { "jsonrpc": "2.0", "method": "tools/call", "id": 1, "params": { "name": "search_global_products", "arguments": { "saved_catalog": "", "query": "I need a crewneck sweater", "context": "buyer looking for sustainable fashion", "limit": 3 } } } ``` ##### {} Response ```json { "jsonrpc": "2.0", "id": 1, "result": { "content": [ { "type": "text", "text": { "offers": [ { "id": "gid://shopify/p/abc123def456", "title": "Organic Cotton Crewneck Sweater", "options": [ { "name": "Size", "values": [ { "value": "S", "availableForSale": true, "exists": true }, { "value": "M", "availableForSale": true, "exists": true }, { "value": "L", "availableForSale": true, "exists": true } ] }, { "name": "Color", "values": [ { "value": "Oatmeal", "availableForSale": true, "exists": true }, { "value": "Forest Green", "availableForSale": true, "exists": true } ] } ], "priceRange": { "min": { "amount": 8900, "currencyCode": "USD" }, "max": { "amount": 8900, "currencyCode": "USD" } }, "availableForSale": true }, { "id": "gid://shopify/p/bcd234efg567", "title": "Recycled Wool Blend Crewneck", "options": [ { "name": "Size", "values": [ { "value": "S", "availableForSale": true, "exists": true }, { "value": "M", "availableForSale": true, "exists": true }, { "value": "L", "availableForSale": true, "exists": true }, { "value": "XL", "availableForSale": true, "exists": true } ] }, { "name": "Color", "values": [ { "value": "Charcoal", "availableForSale": true, "exists": true }, { "value": "Navy", "availableForSale": true, "exists": true } ] } ], "priceRange": { "min": { "amount": 11500, "currencyCode": "USD" }, "max": { "amount": 11500, "currencyCode": "USD" } }, "availableForSale": true }, { "id": "gid://shopify/p/cde345fgh678", "title": "Hemp Cotton Crew Pullover", "options": [ { "name": "Size", "values": [ { "value": "XS", "availableForSale": true, "exists": true }, { "value": "S", "availableForSale": true, "exists": true }, { "value": "M", "availableForSale": true, "exists": true }, { "value": "L", "availableForSale": true, "exists": true } ] } ], "priceRange": { "min": { "amount": 7200, "currencyCode": "USD" }, "max": { "amount": 7200, "currencyCode": "USD" } }, "availableForSale": true } ] } } ] } } ``` Update `ucp_demo.js` to call `searchProducts()` and `displayOffers()`: ## ucp\_demo.js ```javascript import { getAccessToken } from './auth.js'; import { searchProducts, displayOffers } from './search.js'; async function main() { // 1. Authentication const token = await getAccessToken(); // 2. Search the Catalog showCatalog(); const searchResults = await searchProducts(token); if (!searchResults?.offers?.length) return; displayOffers(searchResults.offers); } main().catch(err => console.error('Request failed:', err)); ``` You can now interact with a chat in your terminal to search the catalog by running `node ucp_demo.js`: ## Output ── 1. Authentication ───────────────────────── Scopes: read\_global\_api\_catalog\_search Expires: 6:02:46 PM ── 2. Search the Catalog ───────────────────────── Catalog ID: {your\_catalog\_id} Hello! What are you looking for today? \> I need a men's crew sweatshirt. ── Results ──────────────────────────────────────── \[1] Organic Cotton Crewneck Sweater | $89.00 | Size: S, M, L | Color: Oatmeal, Forest Green \[2] Recycled Wool Blend Crewneck | $115.00 | Size: S, M, L, XL | Color: Charcoal, Navy \[3] Hemp Cotton Crew Pullover | $72.00 | Size: XS, S, M, L *** ## Step 5: Refine results with filters `searchProducts()` accepts optional filters that are passed onto the arguments of [`search_global_products`](https://shopify.dev/docs/agents/catalog/mcp#search_global_products) to narrow results. Update `ucp_demo.js` to pass a few filters: ##### ucp\_demo.js ```javascript import { getAccessToken } from './auth.js'; import { searchProducts } from './search.js'; async function main() { // 1. Authentication const token = await getAccessToken(); // 2. Search the Catalog showCatalog(); const searchResults = await searchProducts(token, { include_secondhand: true, min_price: 50, max_price: 200, ships_to: 'US', }); if (!searchResults?.offers?.length) return; displayOffers(searchResults.offers); } main().catch(err => console.error('Request failed:', err)); ``` ##### {} MCP input reference ```json { "jsonrpc": "2.0", "method": "tools/call", "id": 1, "params": { "name": "search_global_products", "arguments": { "saved_catalog": "", "query": "I need a crewneck sweater", "context": "buyer looking for sustainable fashion", "include_secondhand": true, "min_price": 50, "max_price": 200, "ships_to": "US" } } } ``` ##### {} Response ```json { "jsonrpc": "2.0", "id": 1, "result": { "content": [ { "type": "text", "text": { "offers": [ { "id": "gid://shopify/p/abc123def456", "title": "Organic Cotton Crewneck Sweater", "options": [ { "name": "Size", "values": [ { "value": "S", "availableForSale": true, "exists": true }, { "value": "M", "availableForSale": true, "exists": true }, { "value": "L", "availableForSale": true, "exists": true } ] }, { "name": "Color", "values": [ { "value": "Oatmeal", "availableForSale": true, "exists": true }, { "value": "Forest Green", "availableForSale": true, "exists": true } ] } ], "priceRange": { "min": { "amount": 8900, "currencyCode": "USD" }, "max": { "amount": 8900, "currencyCode": "USD" } }, "availableForSale": true }, { "id": "gid://shopify/p/bcd234efg567", "title": "Recycled Wool Blend Crewneck", "options": [ { "name": "Size", "values": [ { "value": "S", "availableForSale": true, "exists": true }, { "value": "M", "availableForSale": true, "exists": true }, { "value": "L", "availableForSale": true, "exists": true }, { "value": "XL", "availableForSale": true, "exists": true } ] }, { "name": "Color", "values": [ { "value": "Charcoal", "availableForSale": true, "exists": true }, { "value": "Navy", "availableForSale": true, "exists": true } ] } ], "priceRange": { "min": { "amount": 11500, "currencyCode": "USD" }, "max": { "amount": 11500, "currencyCode": "USD" } }, "availableForSale": true }, { "id": "gid://shopify/p/cde345fgh678", "title": "Hemp Cotton Crew Pullover", "options": [ { "name": "Size", "values": [ { "value": "XS", "availableForSale": true, "exists": true }, { "value": "S", "availableForSale": true, "exists": true }, { "value": "M", "availableForSale": true, "exists": true }, { "value": "L", "availableForSale": true, "exists": true } ] } ], "priceRange": { "min": { "amount": 7200, "currencyCode": "USD" }, "max": { "amount": 7200, "currencyCode": "USD" } }, "availableForSale": true } ] } } ] } } ``` *** ## Step 6: Select a product variant Once a buyer picks a result, you'll want them to be able to retrieve variant options from that product to narrow down to their final selection. Create `product.js` which handles fetching product details via [`get_global_product_details`](https://shopify.dev/docs/agents/catalog/mcp#get_global_product_details), displaying them, and walking the buyer through variant selection. ##### product.js ```javascript import { prompt } from './utils.js'; async function getProductDetails(token, upid) { const res = await fetch('https://discover.shopifyapps.com/global/mcp', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ jsonrpc: '2.0', method: 'tools/call', id: 2, params: { name: 'get_global_product_details', arguments: { upid } } }) }); const data = await res.json(); if (data?.result?.content?.[0]?.text) { data.result.content[0].text = JSON.parse(data.result.content[0].text); } return data; } function displayProduct(product, featuredVariant) { const price = `$${(featuredVariant.price.amount / 100).toFixed(2)}`; console.log('\n── 3. Product Details ─────────────────────────────\n'); console.log(` ${featuredVariant.displayName}`); console.log(` ${price} · ${featuredVariant.shop.name}\n`); console.log(` ${product.description}\n`); product.topFeatures.forEach(f => console.log(` · ${f}`)); } async function pickVariant(product, offerVariants) { const defaultOfferVariant = offerVariants.find(v => v.availableForSale) ?? offerVariants[0]; const selected = Object.fromEntries( (defaultOfferVariant?.options ?? product.selectedOptions ?? []).map(o => [o.name, o.value]) ); if (product.options?.length) while (true) { const optionMap = []; console.log('\n Options:'); product.options.forEach(opt => { const lines = opt.values.map(v => { const n = optionMap.length + 1; const marker = selected[opt.name] === v.value ? '●' : '○'; optionMap.push({ optName: opt.name, value: v.value }); return ` [${n}] ${marker} ${v.value}`; }); console.log(`\n ${opt.name}:`); lines.forEach(l => console.log(l)); }); const selectedDesc = product.options.map(o => selected[o.name]).join(' / '); console.log(`\n \x1b[1mSelected: ${selectedDesc}\x1b[0m`); console.log('\n [s] Select this variant [number] Pick an option [b] Back to results'); const action = await prompt('\n > '); const trimmed = action.trim(); if (trimmed === 'b') return null; if (trimmed === 's') break; const chosen = optionMap[parseInt(trimmed) - 1]; if (chosen) selected[chosen.optName] = chosen.value; } const selectedTitle = product.options?.map(o => selected[o.name]).join(' / ') ?? ''; const matchedOfferVariant = offerVariants.find(v => { if (Array.isArray(v.selectedOptions)) return v.selectedOptions.every(o => selected[o.name] === o.value); if (Array.isArray(v.options)) return v.options.every(o => selected[o.name] === o.value); if (v.title) return v.title === selectedTitle; return false; }) ?? offerVariants[0]; return { variantId: matchedOfferVariant?.id?.split('?')[0], checkoutUrl: matchedOfferVariant?.checkoutUrl, }; } // Prompts the user to pick from a list of offers, fetches full product details, // and walks them through variant selection. Returns { variantId, checkoutUrl } // or null if the user goes back to results. export async function selectProduct(token, offers) { const pick = await prompt(`\x1b[1m Lookup details on a result [1-${offers.length}]:\x1b[0m `); const index = parseInt(pick) - 1; const selectedOffer = offers[index]; const offerVariants = selectedOffer.variants ?? []; // The UPID (universal product ID) is the segment after /p/ in the offer ID. const upid = selectedOffer.id.split('/p/')[1]; const details = await getProductDetails(token, upid); const product = details.result.content[0].text.product; // The featured variant is used for display only — actual selection is driven by offerVariants. const featuredVariant = product.variants[0]; displayProduct(product, featuredVariant); const variant = await pickVariant(product, offerVariants); if (variant) console.log(`\n Checkout: ${variant.checkoutUrl}`); return variant; } ``` ##### {} MCP input reference ```json { "jsonrpc": "2.0", "method": "tools/call", "id": 2, "params": { "name": "get_global_product_details", "arguments": { "upid": "" } } } ``` ##### {} Response ```json { "jsonrpc": "2.0", "id": 2, "result": { "content": [ { "type": "text", "text": { "product": { "id": "gid://shopify/p/abc123def456", "title": "Organic Cotton Crewneck Sweater", "description": "A soft crewneck sweater crafted from 100% organic cotton with a relaxed fit for everyday comfort.", "options": [ { "name": "Size", "values": [ { "value": "S", "availableForSale": true, "exists": true }, { "value": "M", "availableForSale": true, "exists": true }, { "value": "L", "availableForSale": true, "exists": true } ] }, { "name": "Color", "values": [ { "value": "Oatmeal", "availableForSale": true, "exists": true }, { "value": "Forest Green", "availableForSale": true, "exists": true } ] } ], "topFeatures": [ "100% organic cotton for breathable comfort", "Relaxed fit with ribbed cuffs and hem", "GOTS certified sustainable production", "Pre-washed for softness", "Classic crewneck design" ], "variants": [ { "id": "gid://shopify/ProductVariant/11111111111?shop=1111111111", "displayName": "Organic Cotton Crewneck Sweater - M / Oatmeal", "availableForSale": true, "price": { "amount": 8900, "currencyCode": "USD" }, "checkoutUrl": "https://ecowear-example.myshopify.com/cart/11111111111:1?_gsid=example123", "selectedOptions": [ { "name": "Size", "value": "M" }, { "name": "Color", "value": "Oatmeal" } ], "shop": { "name": "EcoWear", "onlineStoreUrl": "https://ecowear-example.myshopify.com" } } ] } } } ] } } ``` Update `ucp_demo.js` to use `selectProduct()`: ## ucp\_demo.js ```javascript import { getAccessToken } from './auth.js'; import { searchProducts, displayOffers } from './search.js'; import { selectProduct } from './product.js'; async function main() { // 1. Authentication const token = await getAccessToken(); // 2 & 3. Search and select a variant let variant = null; while (!variant) { const searchResults = await searchProducts(token, { include_secondhand: true, min_price: 50, max_price: 200, ships_to: 'US', }); if (!searchResults?.offers?.length) return; displayOffers(searchResults.offers); variant = await selectProduct(token, searchResults.offers); } } main().catch(err => console.error('Request failed:', err)); ``` Then re-run `node ucp_demo.js` in your terminal and explore the added ability to select variants: ## Output ── 1. Authentication ───────────────────────── Scopes: read\_global\_api\_catalog\_search Expires: 6:02:46 PM ── 2. Search the Catalog ───────────────────────── Catalog ID: {your\_catalog\_id} Hello! What are you looking for today? \> I need a men's crew sweatshirt. ── Results ──────────────────────────────────────── \[1] Organic Cotton Crewneck Sweater | $89.00 | Size: S, M, L | Color: Oatmeal, Forest Green \[2] Recycled Wool Blend Crewneck | $115.00 | Size: S, M, L, XL | Color: Charcoal, Navy \[3] Hemp Cotton Crew Pullover | $72.00 | Size: XS, S, M, L Lookup details on a result \[1-3]: 1 ── 3. Product Details ───────────────────────────── Organic Cotton Crewneck Sweater - M / Oatmeal $89.00 · EcoWear A soft crewneck sweater crafted from 100% organic cotton with a relaxed fit for everyday comfort. · 100% organic cotton for breathable comfort · Relaxed fit with ribbed cuffs and hem · GOTS certified sustainable production · Pre-washed for softness · Classic crewneck design Options: Size: \[1] ○ S \[2] ● M \[3] ○ L Color: \[4] ● Oatmeal \[5] ○ Forest Green Selected: M / Oatmeal \[s] Select this variant \[number] Pick an option \[b] Back to results \> s Checkout: https://ecowear-example.myshopify.com/cart/11111111111:1?\_gsid=example123 At this point the buyer has chosen a variant from their initial query for a single merchant. You'll likely design agentic experiences that are able to refer checkout for multiple products across potentially many merchants. This tutorial keeps things simple by assuming that the buyer is only interested in purchasing this selected product from a single merchant. In the next step, your script will need to use this selection to refer buyers to the merchant storefront to finish their purchase. *** ## Next steps [Finish checkout\ \ ](https://shopify.dev/docs/agents/get-started/checkout) [With a product selected, create a checkout session and direct buyers to the merchant's storefront.](https://shopify.dev/docs/agents/get-started/checkout) ***