feat: get product operation for catalog.lookup#195
Conversation
|
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? Why:
|
|
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. |
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.
df11f2c to
4e75cc1
Compare
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.
@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. |
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.
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.
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
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
…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
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>
search_catalogandlookup_catalogare 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_productis a single-resource operation (part ofdev.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/productMCP:
get_producttoolExample request
{ "id": "prod_abc123", "selected": [ { "name": "Color", "label": "Blue" } ], "preferences": ["Color", "Size"], "context": { "country": "US" } }Only
idis required.selectedandpreferencesare 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
get_product(id: "prod_abc123")— no selections. Server returns the product with featured variant and option map.get_product(id: "prod_abc123", selected: [{name: "Color", label: "Blue"}]). Response narrows:product.selectedconfirms Blue, variants are all Blue, availability on Size values updates to reflect Blue inventory.selected: [{Color: Red}, {Size: 15}]withpreferences: ["Color", "Size"]. No Red/15 exists. Server relaxes from the end ofpreferences— drops Size, keeps Color. Responseproduct.selectedis[{Color: Red}]. Agent diffs request vs responseselected, 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.selectedis the response anchor. It determines the featured variant, the variant subset, and all availability signals. One concept, not three.product.selected. No "adjacent" or "contextually relevant" variants outside the selection. Availability context for non-matching options is carried byoptions[].values[].available/exists, not the variant array.selected_options→optionson variants. Separates variant identity (what a variant is) from user selection state (what the user chose). These were previously conflated under the same name.inputcorrelation moved tolookup_variant. Correlation is a batch-lookup concern, not intrinsic to variants. Base variant type stays clean; operation-specific extensions viaallOf.product) not array. Single-resource semantics — not found is an error (404 /-32602), not an empty result.Checklist