Skip to content

feat: get product operation for catalog.lookup#195

Open
igrigorik wants to merge 1 commit intofeat/catalog-capabilityfrom
feat/catalog-get-product
Open

feat: get product operation for catalog.lookup#195
igrigorik wants to merge 1 commit intofeat/catalog-capabilityfrom
feat/catalog-get-product

Conversation

@igrigorik
Copy link
Contributor

search_catalog and lookup_catalog are discovery operations — they return multiple products with featured variant(s). But once a user picks a product, the agent needs a different interaction: full product detail with all options, real-time availability as the user selects options (Color, Size), and the exact variant to purchase. This is the product detail page (PDP) flow, which is best modelled as a distinct operation — this pattern conforms to common API shapes in the wild.

get_product is a single-resource operation (part of dev.ucp.shopping.catalog.lookup) for servicing purchase flow decisions. It returns one product with a relevant subset of variants, option-level availability signals, and support for interactive variant narrowing.

REST: POST /catalog/product
MCP: get_product tool

Example request

{
  "id": "prod_abc123",
  "selected": [
    { "name": "Color", "label": "Blue" }
  ],
  "preferences": ["Color", "Size"],
  "context": { "country": "US" }
}

Only id is required. selected and preferences are for interactive narrowing.

Example (redacted) response

{
  "product": {
    "id": "prod_abc123",
    "title": "Runner Pro",
    "price_range": {
      "min": { "amount": 12000, "currency": "USD" },
      "max": { "amount": 15000, "currency": "USD" }
    },
    "options": [
      {
        "name": "Color",
        "values": [
          { "label": "Blue",  "available": true,  "exists": true },
          { "label": "Green", "available": false, "exists": true }
        ]
      },
      {
        "name": "Size",
        "values": [
          { "label": "10", "available": true,  "exists": true },
          { "label": "11", "available": false, "exists": false }
        ]
      }
    ],
    "selected": [{ "name": "Color", "label": "Blue" }],
    "variants": [
      {
        "id": "var_abc123_blue_10",
        "sku": "RP-BLU-10",
        "title": "Blue / Size 10",
        "price": { "amount": 12000, "currency": "USD" },
        "availability": { "available": true },
        "options": [
          { "name": "Color", "label": "Blue" },
          { "name": "Size", "label": "10" }
        ],
        "media": [{ "type": "image", "url": "https://cdn.example.com/runner-pro-blue.jpg" }]
      }
    ]
  }
}

Iterative Flow

  1. Agent calls get_product(id: "prod_abc123") — no selections. Server returns the product with featured variant and option map.
  2. User picks Color=Blue. Agent calls get_product(id: "prod_abc123", selected: [{name: "Color", label: "Blue"}]). Response narrows: product.selected confirms Blue, variants are all Blue, availability on Size values updates to reflect Blue inventory.
  3. User picks Size=10. Agent adds to selections. Response returns the exact Blue/10 variant — price, SKU, availability — ready for checkout.
  4. User picks an impossible combination. Agent sends selected: [{Color: Red}, {Size: 15}] with preferences: ["Color", "Size"]. No Red/15 exists. Server relaxes from the end of preferences — drops Size, keeps Color. Response product.selected is [{Color: Red}]. Agent diffs request vs response selected, sees Size was dropped, and can surface that to the user.

Each round-trip is stateless. The agent sends the full selection state, the server returns the full product state.

Key Design Decisions

  • product.selected is the response anchor. It determines the featured variant, the variant subset, and all availability signals. One concept, not three.
  • All returned variants match product.selected. No "adjacent" or "contextually relevant" variants outside the selection. Availability context for non-matching options is carried by options[].values[].available/exists, not the variant array.
  • selected_optionsoptions on variants. Separates variant identity (what a variant is) from user selection state (what the user chose). These were previously conflated under the same name.
  • input correlation moved to lookup_variant. Correlation is a batch-lookup concern, not intrinsic to variants. Base variant type stays clean; operation-specific extensions via allOf.
  • Singular response (product) not array. Single-resource semantics — not found is an error (404 / -32602), not an empty result.

Checklist

  • New feature (non-breaking change which adds functionality)
  • 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

Add a dedicated single-product retrieval operation (get_product / POST /catalog/product) to the
Catalog Lookup capability, separating two fundamentally different access patterns:

- lookup_catalog: batch identifier resolution (cart validation, wishlists, list display)
- get_product: single-product detail with interactive variant narrowing (PDP rendering)

The core addition is interactive option selection via `selected` and `preferences` parameters,
modeling the standard product detail page interaction where a user progressively picks options
(Color, Size) and the UI updates availability in real time.

Option selection:
- `selected`: partial option selections the user has made so far
- `preferences`: relaxation priority order — when no variant matches all selections, the server
  drops options from the end of this list first, preserving higher-priority choices
- Response always includes `product.selected`: the effective option selections that determine
  the featured variant, the variant subset, and all availability signals
- Clients detect relaxation by diffing their request against product.selected

Availability signals on option values (relative to product.selected):
- available=true, exists=true → purchasable (selectable)
- available=false, exists=true → out of stock (disabled/strikethrough)
- available=false, exists=false → no variant for this combination (hidden)

Variant semantics:
- All returned variants match product.selected — this is the filtering anchor, not just
  the availability anchor
- Rename variant `selected_options` → `options` to separate variant identity (what a variant IS)
  from user selection state (what the user CHOSE)
- Move `input` correlation from base Variant into operation-specific `lookup_variant` extension
  via allOf, since correlation is a lookup concern not intrinsic to variants
@igrigorik igrigorik added this to the Working Draft milestone Feb 20, 2026
@igrigorik igrigorik self-assigned this Feb 20, 2026
@igrigorik igrigorik added the TC review Ready for TC review label Feb 20, 2026
@igrigorik igrigorik changed the title Feat: get product operation for catalog.lookup feat: get product operation for catalog.lookup Feb 20, 2026

#### Product Not Found

When the identifier does not resolve to a product, return HTTP 404:

Choose a reason for hiding this comment

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

REST docs specify POST /catalog/product returns HTTP 404 for not found, but rest.openapi.json only declares a 200 response for this operation (no 404, no 400 for invalid selections/preferences, no 401/403).
Suggested fix: update OpenAPI responses for /catalog/product to include at least 400, 401, 403, 404, and reference the standard error schema.

| **Batch Lookup** | `lookup_catalog` / `POST /catalog/lookup` | Retrieve multiple products by identifier. |
| **Get Product** | `get_product` / `POST /catalog/product` | Retrieve full detail for a single product. |

Both operations accept product and variant identifiers. They differ in cardinality

Choose a reason for hiding this comment

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

The “Both operations accept product and variant identifiers” table implies get_product supports the same identifier surface as lookup_catalog (SKU, URL, handle, etc.), but the later Get Product → Supported Identifiers section says “The id parameter accepts a single product ID or variant ID.” These are materially different guarantees.

Suggested fix: explicitly state:
“MUST support product ID + variant ID”
“MAY support SKU/URL/handle/etc.” (list optional identifiers)
and add a normative note: identifiers are opaque strings and MUST NOT be dereferenced or fetched as URLs.

"type": "object",
"allOf": [{ "$ref": "types/product.json" }],
"properties": {
"selected": {

Choose a reason for hiding this comment

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

detail_product.selected is optional and the description says “Present when the request includes selected options,” but the docs in lookup.md say the response MUST include product.selected and that it determines featured variant + availability anchor.

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.

2 participants