Skip to content

Conversation

@igrigorik
Copy link
Contributor

@igrigorik igrigorik commented Jan 15, 2026

Summary

Introduces the dev.ucp.shopping.catalog capability, enabling platforms to search merchant product catalogs and retrieve product/variant details. This fills the "discovery gap" before checkout—while checkout assumes you already have a variant ID, catalog enables scenarios like "find me blue running shoes under $150" that lead to cart building and purchase.

The capability provides two operations: free-text search with filters and pagination, and direct product/variant lookup by ID. Both return a consistent Product structure containing variants, media, options, and pricing that seamlessly connects to checkout's line_items[].item.id.

Motivation

UCP's checkout capability assumes the platform already knows what to buy. But agents and platforms need to discover products first—browsing catalogs, comparing options, filtering by price or category. Even when a product ID is already known (e.g., from a product feed integration), platforms need live lookups to fetch current facts—price, availability, and other details that may have changed. Without a standardized catalog interface, every integration requires custom product APIs or out-of-band discovery mechanisms—N custom integrations instead of one standardized protocol.

Goals

  • Enable product discovery via free-text search with category and price filters
  • Support semantic search via optional intent context field
  • Provide real-time product/variant lookup for cart validation and deep links
  • Return variant IDs directly usable in checkout line_items[].item.id
  • Establish consistent Product/Variant schema for cross-merchant compatibility

Non-Goals

  • Inventory management or stock updates (read-only catalog access)
  • Product creation, modification, or deletion (merchant-side operations)
  • Recommendations or personalization algorithms (merchant implementation detail)
  • Full-text search ranking or relevance tuning (merchant implementation detail)

Detailed Design

Operations

Operation Description
search_catalog Free-text search with filters, context, and pagination
get_catalog_item Retrieve product or variant by Global ID

Data Structures

Product (catalog entry):

├─ id, title, description, url, category
├─ price: PriceRange (min/max across variants)
├─ media[]: images, videos, 3D models (first = featured)
├─ options[]: dimensions like Size, Color
├─ variants[]: purchasable SKUs (first = featured)
├─ rating: aggregate reviews
└─ metadata: merchant-defined extensions

Variant (purchasable SKU):

├─ id: used as item.id in checkout
├─ sku, barcode: inventory identifiers
├─ title, price, availability
├─ selected_options[]: option values for this variant
├─ media[], rating, tags, metadata
└─ seller: optional marketplace context

Key Behaviors

Ordering Convention: media[] and variants[] are ordered arrays. Merchants SHOULD return featured items first; platforms SHOULD treat first element as featured.

ID Resolution: get_catalog_item accepts product OR variant IDs:

  • Product ID → returns product with representative variant set
  • Variant ID → returns parent product with only requested variant

Error Handling: NOT_FOUND returns HTTP 200 with messages[].type: "error" (not 404). All catalog errors use severity: "recoverable" for programmatic handling.

Transport Bindings

REST:

POST /catalog/search     → search_catalog
GET  /catalog/item/{id}  → get_catalog_item

MCP (JSON-RPC):

search_catalog
get_catalog_item

Risks and Mitigations

  • Complexity: Adding another capability increases protocol surface area. Mitigation: Catalog is cleanly scoped with only 2 operations and reuses existing patterns (context, messages, pagination).
  • Schema Drift: Product schemas vary wildly across merchants. Mitigation: Core fields are minimal and universal; metadata and additionalProperties enable merchant extensions without protocol changes.
  • Search Quality: Free-text search results depend entirely on merchant implementation. Mitigation: Protocol defines interface only; merchants own ranking/relevance. intent field enables semantic hints without prescribing implementation.
  • Backward Compatibility: N/A—new capability, no breaking changes to existing checkout flow.
  • Performance: Large catalogs could return excessive data. Mitigation: Pagination with default 10, max 25 results; representative variant sets for products with many variants.

Type of change

  • New feature (non-breaking change which adds functionality)

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • New and existing unit tests pass locally with my changes

Introduces `dev.ucp.shopping.catalog` capability enabling platforms to search
business product catalog and perform targeted product+variant lookups.

Checkout capability assumes the platform already knows what item to buy (via
variant ID). Catalog capability fills the discovery gap—enabling scenarios like
"find me blue running shoes under $150" that lead to cart building and checkout.

  Product (catalog entry)
    ├─ id, title, description, url, category
    ├─ price: PriceRange (min/max across variants)
    ├─ media[]: images, videos, 3D models (first = featured)
    ├─ options[]: dimensions like Size, Color
    ├─ variants[]: purchasable SKUs (first = featured)
    ├─ rating: aggregate reviews
    └─ metadata: merchant-defined data

  Variant (purchasable SKU)
    ├─ id: used as item.id in checkout
    ├─ sku, barcode: inventory identifiers
    ├─ title: "Blue / Large"
    ├─ price: Price (amount + currency in minor units)
    ├─ availability: { available: bool }
    ├─ selected_options[]: option values for this variant
    ├─ media[], rating, tags, metadata
    └─ seller: optional marketplace context

  - Free-text query with semantic search support
  - Filters: category (string), price (min/max in minor units)
  - Context: country, region, postal_code, intent
  - Cursor-based pagination (default 10, max 25)

  - Accepts product ID OR variant ID
  - Always returns parent product with context
  - Product ID → variants MAY be representative set
  - Variant ID → variants contains only requested variant
  - NOT_FOUND returns HTTP 200 with error message (not 404)

Location and market context unified into reusable types/context.json:

  {
    "country": "US",      // ISO 3166-1 alpha-2
    "region": "CA",       // State/province
    "postal_code": "..."  // ZIP/postal
  }

Catalog extends with 'intent' for semantic search hints.

REST:
  POST /catalog/search     → search_catalog
  GET  /catalog/item/{id}  → get_catalog_item

MCP (JSON-RPC):
  search_catalog
  get_catalog_item
  catalog.json references `../ucp.json#/$defs/response` for its response
  envelope, but this $def only existed in spec/ (manually added) - not in
  source/. This caused source/spec drift: regenerating spec/ would lose
  the generic response definition.

  Fix:
  - Add generic `response` $def to source/schemas/ucp.json
  - Change `response_checkout` and `response_order` to $ref the generic
  Inline object definitions in search_request.filters weren't rendered
  in generated docs (showed as plain "object" without properties).

  Fix by extracting to referenceable schemas:
  - search_filters.json: category + price filter definitions
  - price_filter.json: min/max integer bounds (distinct from price_range
    which uses full Price objects with currency)
@igrigorik igrigorik requested a review from sinhanurag January 15, 2026 17:06
@igrigorik igrigorik self-assigned this Jan 15, 2026
@igrigorik igrigorik requested a review from a team January 15, 2026 17:06
@igrigorik igrigorik added the TC review Ready for TC review label Jan 15, 2026
@igrigorik igrigorik added this to the Working Draft milestone Jan 16, 2026
@amithanda
Copy link

Thanks Ilya. I am assuming we will add local store support in a later PR?
I will do a pass by Tuesday.

Copy link

@knightlin-shopify knightlin-shopify left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the expected language handling for text content coming from the business (e.g., product/variant descriptions and message content)? We should require an explicit content language provided by the platform/agent (via context) that reflects the user’s selected display language which cannot be inferred from context.country. The agent should render the content as-is and avoid automatic translation, since some fields (e.g., product name, variant name, legal/safety/liability disclaimers) must not be translated and may appear mixed within the same text blob.


`media` and `variants` are ordered arrays. Businesses SHOULD return the featured
image and default variant as the first element. Platforms SHOULD treat the first
element as the featured item for display.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clarify that media/variants ordering should be query-aware in search results (e.g., "yellow shoes" → yellow variant first), but default-based for direct lookups?


Messages communicate business outcomes and provide context:

| Type | When to Use | Example Codes |

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code defined here uses UPPER_SNAKE_CASE for error codes, but the schema examples use snake_case.


**Empty Search**

When search finds no matches, return an empty array without messages.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we return a more explicit info message with the proper code (not_found)? This would make it clearer for the agent/platform to handle and avoid retrying the request.


### Business Outcomes

All application-level outcomes return HTTP 200 with the UCP envelope and optional

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GET by ID is resource retrieval - if the resource doesn't exist, the URI is invalid → should return HTTP 404.

  • POST /catalog/search with no matches -> 200 OK (query succeeded, empty results)
  • GET /catalog/item/{invalid_id} -> 404 Not Found (resource doesn't exist)
  • GET /catalog/item/{valid_id} that's out of stock -> 200 + message (resource exists, business constraint)

```json
{
"ucp": {...},
"product": {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Add required fields description and price.

"$ref": "price_filter.json"
}
},
"additionalProperties": true

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see not all newly add schema has this set, additionalProperties default is true, just to double check the expect value across all schema file added.

"required": [
"id",
"title",
"description",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should description optional, which should inherit from the product it belongs to?

"format": "uri",
"description": "Canonical variant page URL."
},
"category": {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be product only property?

},
"description": "Variant media (images, videos, 3D models). First item is the featured media for listings."
},
"rating": {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be product only property?

"$ref": "rating.json",
"description": "Variant rating."
},
"tags": {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be product only property?

Copy link

@raginpirate raginpirate left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again... incredible to see these primitives come to life so "simply"!
Just a couple questions about unification I spotted.

"response_checkout": {
"title": "UCP Checkout Response",
"description": "UCP metadata for checkout responses.",
"response": {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to clarify how this would work once you target the working branch: are we thinking that capabilities exposed through discovery would be the same as cart and checkout, or would it be a unique subset? I'm imagining this should be isolated and not explain how I do fulfillment from product discovery.

Either way, if we choose to include all capabilities, should it really expose their configs like it does at checkout, or just negotiate versioning?

Comment on lines +17 to +21
"currency": {
"type": "string",
"description": "ISO 4217 currency code (e.g., 'USD', 'EUR', 'GBP').",
"pattern": "^[A-Z]{3}$"
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checkout doesn't push currency on every amount; it uses bare amounts and has a top-level contextual currency. Should we be thinking the same way for catalog?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

TC review Ready for TC review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants