Skip to content

feat: get product operation for catalog.lookup#195

Merged
igrigorik merged 8 commits intomainfrom
feat/catalog-get-product
Apr 3, 2026
Merged

feat: get product operation for catalog.lookup#195
igrigorik merged 8 commits intomainfrom
feat/catalog-get-product

Conversation

@igrigorik
Copy link
Copy Markdown
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

@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
Comment thread docs/specification/catalog/rest.md Outdated
Comment thread docs/specification/catalog/lookup.md Outdated
Comment thread source/schemas/shopping/catalog_lookup.json
@amithanda
Copy link
Copy Markdown
Contributor

amithanda commented Mar 1, 2026

Should we add Shipping speed, price & options (e.g pickup in store, delivery etc) to the response, if location related buyer user context is provided?
Should we add return policy for the product?

Why:

  • In addition to Product price, variants and availablity (which we are addressing currently), shipping and returns are the other important signals for user's purchase considerations which are useful to be shown on the PDP.

@alex-jansen
Copy link
Copy Markdown

Note that #222 is also relevant in the context of catalog lookups as part of important regulatory product information that should be returned in product lookups.

Comment thread source/schemas/shopping/catalog_lookup.json Outdated
Comment thread source/schemas/shopping/catalog_lookup.json
Comment thread source/schemas/shopping/catalog_lookup.json
Comment thread source/schemas/shopping/catalog_lookup.json
Base automatically changed from feat/catalog-capability to main March 8, 2026 05:08
@igrigorik igrigorik requested review from a team as code owners March 8, 2026 05:08
Comment thread source/schemas/shopping/types/selected_option.json
  Adds get_product (POST /catalog/product / MCP get_product tool) to the
  Catalog Lookup capability, enabling interactive product detail pages
  with variant selection and real-time availability signals.

  == Problem

  Agents resolving a product from search or lookup need a way to fetch
  full product detail for a purchase decision — options, availability
  per option combination, and variant narrowing as the user selects
  options. lookup_catalog is batch-oriented and returns a single featured
  variant per product; it has no concept of option selection state.

  == Solution

  get_product is a single-resource operation that accepts a known product
  or variant ID and optional selection state:

  - `selected`: partial or full option selections (e.g., Color=Blue)
  - `preferences`: relaxation priority when no exact match exists

  The response returns the product with a relevant variant subset and
  availability signals (available/exists) on each option value, relative
  to the current selections. This is the contract for product detail page
  rendering.

  == Key design decisions

  1. Lookup capability scope: get_product lives under
     dev.ucp.shopping.catalog.lookup, not a separate capability. When
     Lookup is advertised, both lookup_catalog and get_product MUST be
     available.

  2. Identifier surface: get_product accepts product ID or variant ID
     only. No SKU/URL/handle — that's lookup_catalog's resolver role.
     get_product is for known IDs.

  3. selected optionality: MUST be present when product has configurable
     options. MAY be empty or omitted for optionless products. No empty
     array ceremony for single-variant products.

  4. Availability signals scoped to detail: available/exists fields live
     on detail_option_value.json, not the shared option_value.json.
     Batch lookup responses don't carry selection-relative signals.

  5. Error model: product-not-found is an application outcome (HTTP 200 /
     JSON-RPC result with ucp.status: "error"), not a transport error.
     Aligned with #216 error model — handler ran, reports its result via
     UCP envelope.

  6. Relaxation: when no variant matches all selections, server drops
     options from the end of the preferences list. Agents detect
     relaxation by diffing request selected vs response product.selected.
  Servers can expose stable identifiers on option values, enabling
  ID-based matching instead of string comparison on name/label.
  Optional and additive — name + label remain required for display.

  Resolves PR feedback from @ihoosain on selected_option matching.
@igrigorik igrigorik force-pushed the feat/catalog-get-product branch from df11f2c to 4e75cc1 Compare April 2, 2026 19:51
  Price range, category, and other filter criteria apply to lookup
  and product detail, not just search. Reuses search_filters.json
  on both lookup_request and get_product_request — same shape,
  AND semantics.
@igrigorik
Copy link
Copy Markdown
Contributor Author

igrigorik commented Apr 2, 2026

Should we add shipping speed, price & options (e.g pickup in store, delivery etc) to the response, if location related buyer user context is provided? Should we add return policy for the product?

@amithanda I think all of those deserve further exploration and some can be modelled as extensions (e.g. pickup-in-store) while others may make sense in core schema. That said, all of them are not specific to get-product operation but catalog as a whole, and I don't think we need to hold this PR; let's tackle these as followup PRs.


I've rebased and addressed all the outstanding feedback. I believe this should be good to merge.

Comment thread docs/specification/catalog/rest.md
Comment thread source/schemas/shopping/catalog_lookup.json
  get_product_response required ["ucp", "product"] which made error
  responses (ucp + messages, no product) schema-invalid. Apply the
  same oneOf pattern used by checkout and cart — success shape OR
  error_response — in both REST OpenAPI and MCP OpenRPC bindings.
  Document filter semantics for lookup and get_product — filters
  apply after identifier resolution / option selection, same schema
  and AND semantics as search. Cross-references search docs for
  filter schema to avoid duplication.
@igrigorik igrigorik requested a review from amithanda April 3, 2026 15:19
  The oneOf response schema (get_product_response | error_response)
  generates anchor links to #catalog-lookup-get-product-response and
  #error-response. Add corresponding entity sections in both REST
  and MCP binding docs to resolve broken links.
@igrigorik igrigorik merged commit 7bfc7b9 into main Apr 3, 2026
11 checks passed
@igrigorik igrigorik deleted the feat/catalog-get-product branch April 3, 2026 17:27
@jingyli jingyli added the enhancement New feature or request label Apr 8, 2026
igrigorik added a commit that referenced this pull request Apr 10, 2026
  Completes a rename decided in PR #195 (get_product operation) but never
  executed on variant.json. The get_product operation introduced three
  distinct concepts that were previously conflated:

    request.selected  — what the user chose (input parameter)
    product.selected  — what the server resolved (response, post-relaxation)
    variant.options   — what the variant IS (intrinsic, immutable)

  A variant's option values (Color: Blue, Size: Large) are intrinsic
  identity — they don't change based on user selections or server
  relaxation. The old name "selected_options" implied a relationship to
  user selection state that doesn't exist at the variant level.

  Schema: variant.selected_options → variant.options
  Docs: update variant examples in mcp.md and rest.md
igrigorik added a commit that referenced this pull request Apr 11, 2026
Completes a rename decided in PR #195 (get_product operation) but never
  executed on variant.json. The get_product operation introduced three
  distinct concepts that were previously conflated:

    request.selected  — what the user chose (input parameter)
    product.selected  — what the server resolved (response, post-relaxation)
    variant.options   — what the variant IS (intrinsic, immutable)

  A variant's option values (Color: Blue, Size: Large) are intrinsic
  identity — they don't change based on user selections or server
  relaxation. The old name "selected_options" implied a relationship to
  user selection state that doesn't exist at the variant level.

  Schema: variant.selected_options → variant.options
  Docs: update variant examples in mcp.md and rest.md
jingyli pushed a commit to jingyli/ucp that referenced this pull request Apr 13, 2026
…l#353)

Completes a rename decided in PR Universal-Commerce-Protocol#195 (get_product operation) but never
  executed on variant.json. The get_product operation introduced three
  distinct concepts that were previously conflated:

    request.selected  — what the user chose (input parameter)
    product.selected  — what the server resolved (response, post-relaxation)
    variant.options   — what the variant IS (intrinsic, immutable)

  A variant's option values (Color: Blue, Size: Large) are intrinsic
  identity — they don't change based on user selections or server
  relaxation. The old name "selected_options" implied a relationship to
  user selection state that doesn't exist at the variant level.

  Schema: variant.selected_options → variant.options
  Docs: update variant examples in mcp.md and rest.md
jingyli added a commit that referenced this pull request Apr 13, 2026
Completes a rename decided in PR #195 (get_product operation) but never
  executed on variant.json. The get_product operation introduced three
  distinct concepts that were previously conflated:

    request.selected  — what the user chose (input parameter)
    product.selected  — what the server resolved (response, post-relaxation)
    variant.options   — what the variant IS (intrinsic, immutable)

  A variant's option values (Color: Blue, Size: Large) are intrinsic
  identity — they don't change based on user selections or server
  relaxation. The old name "selected_options" implied a relationship to
  user selection state that doesn't exist at the variant level.

  Schema: variant.selected_options → variant.options
  Docs: update variant examples in mcp.md and rest.md

Co-authored-by: Ilya Grigorik <ilya@grigorik.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request TC review Ready for TC review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants