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.
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.
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.
Anchor to What you'll learnWhat 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
Anchor to RequirementsRequirements
- Complete the Authenticate your agent and Profile tutorials
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.
-
In Dev Dashboard click Catalogs from the sidebar.
-
Click Create a catalog.
-
Keep the defaults, which place no bounds on price and search across all of Shopify's products.

-
Click Save catalog.
-
On the Catalogs landing page, click Copy URL.
Anchor to Step 2: Set up searchStep 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
Update ucp_demo.js to import and call showCatalog():
ucp_demo.js
Run node ucp_demo.js again to see the changes:
Output
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
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:
search.js
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
{
"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
}
}
}{} Response
{
"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
You can now interact with a chat in your terminal to search the catalog by running node ucp_demo.js:
Output
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:
ucp_demo.js
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
{
"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"
}
}
}{} Response
{
"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.
product.js
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
{
"jsonrpc": "2.0",
"method": "tools/call",
"id": 2,
"params": {
"name": "get_global_product_details",
"arguments": {
"upid": "<UPID>"
}
}
}{} Response
{
"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
Then re-run node ucp_demo.js in your terminal and explore the added ability to select variants:
Output
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.