Skip to main content

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 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 and MCP binding. Tool names, request and response shapes differ, so follow this documentation to build with Catalog on Shopify.


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


Anchor to Step 1: Create a custom catalogStep 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 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
  4. Click Save catalog.

  5. On the Catalogs landing page, click Copy URL.


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

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

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}

Anchor to Step 3: Create the prompt utilityStep 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

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); }));
}

Anchor to Step 4: Search for productsStep 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 tool:

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);
}
{
"jsonrpc": "2.0",
"method": "tools/call",
"id": 1,
"params": {
"name": "search_global_products",
"arguments": {
"saved_catalog": "<CATALOG_ID>",
"query": "I need a crewneck sweater",
"context": "buyer looking for sustainable fashion",
"limit": 3
}
}
}
{
"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

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

Anchor to Step 5: Refine results with filtersStep 5: Refine results with filters

searchProducts() accepts optional filters that are passed onto the arguments of search_global_products to narrow results. Update ucp_demo.js to pass a few filters:

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));
{
"jsonrpc": "2.0",
"method": "tools/call",
"id": 1,
"params": {
"name": "search_global_products",
"arguments": {
"saved_catalog": "<CATALOG_ID>",
"query": "I need a crewneck sweater",
"context": "buyer looking for sustainable fashion",
"include_secondhand": true,
"min_price": 50,
"max_price": 200,
"ships_to": "US"
}
}
}
{
"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
}
]
}
}
]
}
}

Anchor to Step 6: Select a product variantStep 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, displaying them, and walking the buyer through variant selection.

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;
}
{
"jsonrpc": "2.0",
"method": "tools/call",
"id": 2,
"params": {
"name": "get_global_product_details",
"arguments": {
"upid": "<UPID>"
}
}
}
{
"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

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.



Was this page helpful?