From df11f2c797188cb0d48e37aec423b92f764e8bc1 Mon Sep 17 00:00:00 2001 From: Ilya Grigorik Date: Thu, 19 Feb 2026 22:21:07 -0800 Subject: [PATCH] feat: get-product for single product+variants retrieval MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/specification/catalog/index.md | 31 ++- docs/specification/catalog/lookup.md | 129 +++++++++- docs/specification/catalog/mcp.md | 229 ++++++++++++++++-- docs/specification/catalog/rest.md | 199 ++++++++++++++- docs/specification/catalog/search.md | 17 ++ source/schemas/shopping/catalog_lookup.json | 98 +++++++- .../schemas/shopping/types/option_value.json | 8 + source/schemas/shopping/types/variant.json | 13 +- source/services/shopping/mcp.openrpc.json | 21 ++ source/services/shopping/rest.openapi.json | 43 ++++ 10 files changed, 730 insertions(+), 58 deletions(-) diff --git a/docs/specification/catalog/index.md b/docs/specification/catalog/index.md index 5193bd87..fa1fcf85 100644 --- a/docs/specification/catalog/index.md +++ b/docs/specification/catalog/index.md @@ -24,6 +24,7 @@ This enables product discovery before checkout, supporting use cases like: * Free-text product search * Category and filter-based browsing * Batch product/variant retrieval by identifier +* Single-product detail for purchase decisions * Price comparison across variants ## Capabilities @@ -31,13 +32,13 @@ This enables product discovery before checkout, supporting use cases like: | Capability | Description | | :--- | :--- | | [`dev.ucp.shopping.catalog.search`](search.md) | Search for products using query text and filters. | -| [`dev.ucp.shopping.catalog.lookup`](lookup.md) | Retrieve products or variants by identifier. | +| [`dev.ucp.shopping.catalog.lookup`](lookup.md) | Retrieve products or variants by identifier. Supports batch lookup and single-product detail. | ## Key Concepts * **Product**: A catalog item with title, description, media, and one or more variants. -* **Variant**: A purchasable item with specific option selections (e.g., "Blue / +* **Variant**: A purchasable item with specific option values (e.g., "Blue / Large"), price, and availability. * **Price**: Price values include both amount (in minor currency units) and currency code, enabling multi-currency catalogs. @@ -74,19 +75,31 @@ currency; response prices include explicit currency codes. A catalog item representing a sellable item with one or more purchasable variants. `media` and `variants` are ordered arrays. Businesses SHOULD return the most -relevant variant and image first—default for lookups, best match based on query -and context for search. Platforms SHOULD treat the first element as featured. +relevant variant and image first. Platforms SHOULD treat the first element as +featured. + +Variant cardinality varies by operation: + +* **Search**: Multiple products with one featured variant each. +* **Batch Lookup**: Multiple products with variants matched by input + identifiers — see [Client Correlation](lookup.md#client-correlation). +* **Get Product**: Single product with featured variant and a relevant + subset, filtered by option selections when provided. {{ schema_fields('types/product', 'catalog') }} +Operation-specific extensions to the base product are defined in the +[Lookup capability schema](lookup.md): `get_product` responses add +`selected` (effective option selections after relaxation); `lookup_catalog` +responses extend variants with `input` correlation. + ### Variant -A purchasable item with specific option selections, price, and availability. +A purchasable item with specific option values, price, and availability. -In lookup responses, each variant carries an `input` array for correlation: -which request identifiers resolved to this variant, and whether the match -was `exact` or `featured` (server-selected). See -[Client Correlation](lookup.md#client-correlation) for details. +Each variant carries an `options` array describing the option values that define +it (e.g., Color: Blue, Size: Large). These are intrinsic to the variant—they +describe what the variant IS, independent of user selections. `media` is an ordered array. Businesses SHOULD return the featured variant image as the first element. Platforms SHOULD treat the first element as featured. diff --git a/docs/specification/catalog/lookup.md b/docs/specification/catalog/lookup.md index 34207e05..e3bcbb09 100644 --- a/docs/specification/catalog/lookup.md +++ b/docs/specification/catalog/lookup.md @@ -19,13 +19,32 @@ * **Capability Name:** `dev.ucp.shopping.catalog.lookup` Retrieves products or variants by identifier. Use this when you already have -identifiers (e.g., from a saved list, deep links, or cart validation). +identifiers (e.g., from a saved list, deep links, cart validation, or a selected +product for detail rendering). -## Operation +## Operations -| Operation | Description | -| :--- | :--- | -| **Lookup Catalog** | Retrieve products or variants by identifier. | +| Operation | Tool / Endpoint | Description | +| :--- | :--- | :--- | +| **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 +and response shape: + +| Concern | `lookup_catalog` | `get_product` | +| :--- | :--- | :--- | +| **Input** | `ids[]` — product or variant ID, SKU, URL, etc. | `id` — single identifier | +| **Purpose** | Resolve identifiers to products | Detailed product information for purchase decisions, optionally filtered by option selection | +| **Variants** | One featured variant per product | Featured variant and relevant subset, filtered by option selections | + +Use `lookup_catalog` when you have identifiers to resolve or display in a list. +Use `get_product` when a user has selected a specific product and needs full +detail, including interactive variant selection, for a purchase decision. + +--- + +## Batch Lookup (`lookup_catalog`) ### Supported Identifiers @@ -68,6 +87,9 @@ with an appropriate error (HTTP 400 `request_too_large` for REST, JSON-RPC * **`featured`**: Identifier resolved to the parent product; server selected this variant as representative (e.g., product ID, handle). +Use [`get_product`](#get-product-get_product) for variant selection, option +availability, and complete product context. + ### Request {{ extension_schema_fields('catalog_lookup.json#/$defs/lookup_request', 'catalog') }} @@ -76,7 +98,100 @@ with an appropriate error (HTTP 400 `request_too_large` for REST, JSON-RPC {{ extension_schema_fields('catalog_lookup.json#/$defs/lookup_response', 'catalog') }} +--- + +## Get Product (`get_product`) + +Retrieves current product state for a single identifier, with support for +interactive variant selection and real-time availability signals. This is the +authoritative source for purchase decisions. + +### Supported Identifiers + +The `id` parameter accepts a single product ID or variant ID. + +### Resolution Behavior + +The response returns the product with complete context (title, description, +media, options) and a **subset of variants matching +[`product.selected`](#option-selection)**: + +* **Product ID**: `variants` SHOULD contain the featured variant and other + variants matching `product.selected`. When the request includes `selected` + options, this narrows the subset to variants matching the client's choices. +* **Variant ID**: The requested variant MUST be the first element (featured). + `product.selected` reflects that variant's options. Remaining variants + match the same effective selections. + +### Response Shape + +The response contains a singular `product` object (not an array). This reflects +the single-resource semantics of the operation. When the identifier is not found, +the response MUST return an error (HTTP 404 for REST, JSON-RPC `-32602` for MCP) +rather than an empty result. + +### Option Selection + +The `selected` and `preferences` parameters enable interactive variant +narrowing: the core product detail page interaction where a user progressively selects options +(Color, Size, etc.) and the UI updates availability in real time. + +#### Input + +* **`selected`**: Array of option selections (e.g., `[{"name": "Color", "label": "Red"}]`). + Partial selections are valid; the client sends whatever the user has chosen so far. + Each option name MUST appear at most once. +* **`preferences`**: Option names in relaxation priority order (e.g., + `["Color", "Size"]`). When no variant matches all selections, the server drops + options from the **end** of this list first, keeping higher-priority selections + intact. Optional; if omitted, the server uses its own relaxation heuristic. + +#### Output: Effective Selections + +The response MUST include `product.selected`: the effective option +selections that determine the featured variant, the variant subset, and +all availability signals. When the request omits `selected`, the server +determines the initial selections (typically the featured variant's own +options). When the request includes `selected`, effective selections +reflect the server's resolution after any relaxation. + +Clients that send `selected` detect relaxation by diffing their request +against `product.selected`: + +* **No relaxation**: Response `selected` matches the request — all + selections resolved to at least one variant. +* **Relaxation occurred**: Response `selected` is a subset of the + request — the server dropped unresolvable options per `preferences` + priority. + +#### Output: Availability Signals + +Option values in the response SHOULD include availability signals +relative to `product.selected`: + +| `available` | `exists` | Meaning | UI Treatment | +| :--- | :--- | :--- | :--- | +| `true` | `true` | In stock — purchasable | Selectable | +| `false` | `true` | Out of stock — variant exists but unavailable | Disabled / strikethrough | +| `false` | `false` | No variant for this combination | Hidden or visually distinct | + +These fields appear on each option value in `product.options[].values[]`. They +reflect availability **relative to the effective selections**. Changing a +selection changes the availability map. + +### Request + +{{ extension_schema_fields('catalog_lookup.json#/$defs/get_product_request', 'catalog') }} + +### Response + +{{ extension_schema_fields('catalog_lookup.json#/$defs/get_product_response', 'catalog') }} + +--- + ## Transport Bindings -* [REST Binding](rest.md#post-cataloglookup): `POST /catalog/lookup` -* [MCP Binding](mcp.md#lookup_catalog): `lookup_catalog` tool +* [REST Binding](rest.md#post-cataloglookup): `POST /catalog/lookup` (batch) +* [REST Binding](rest.md#post-catalogproduct): `POST /catalog/product` (single) +* [MCP Binding](mcp.md#lookup_catalog): `lookup_catalog` tool (batch) +* [MCP Binding](mcp.md#get_product): `get_product` tool (single) diff --git a/docs/specification/catalog/mcp.md b/docs/specification/catalog/mcp.md index 4511f1d8..3786d54a 100644 --- a/docs/specification/catalog/mcp.md +++ b/docs/specification/catalog/mcp.md @@ -49,7 +49,7 @@ Businesses advertise MCP transport availability through their UCP profile at }, { "name": "dev.ucp.shopping.catalog.lookup", - "version": "2026-01-11", + "version": "2026-02-01", "spec": "https://ucp.dev/specification/catalog/lookup", "schema": "https://ucp.dev/schemas/shopping/catalog_lookup.json" } @@ -96,7 +96,8 @@ version compatibility checking and capability negotiation. | Tool | Capability | Description | | :--- | :--- | :--- | | `search_catalog` | [Search](search.md) | Search for products. | -| `lookup_catalog` | [Lookup](lookup.md) | Lookup one or more products or variants by identifier. | +| `lookup_catalog` | [Lookup](lookup.md) | Batch lookup products or variants by identifier. | +| `get_product` | [Lookup](lookup.md#get-product-get_product) | Get full details for a single product. | ### `search_catalog` @@ -204,7 +205,7 @@ Maps to the [Catalog Search](search.md) capability. "description": { "plain": "Size 10 variant" }, "price": { "amount": 12000, "currency": "USD" }, "availability": { "available": true }, - "selected_options": [ + "options": [ { "name": "Size", "label": "10" } ], "tags": ["running", "road", "neutral"], @@ -295,7 +296,7 @@ The `catalog.ids` parameter accepts an array of identifiers and optional context "version": "2026-01-11", "capabilities": { "dev.ucp.shopping.catalog.lookup": [ - {"version": "2026-01-11"} + {"version": "2026-02-01"} ] } }, @@ -384,10 +385,10 @@ response MAY include informational messages indicating which identifiers were no "result": { "structuredContent": { "ucp": { - "version": "2026-01-11", + "version": "2026-02-01", "capabilities": { "dev.ucp.shopping.catalog.lookup": [ - {"version": "2026-01-11"} + {"version": "2026-02-01"} ] } }, @@ -419,6 +420,200 @@ response MAY include informational messages indicating which identifiers were no } ``` +### `get_product` + +Maps to the [Catalog Lookup](lookup.md#get-product-get_product) capability. Returns a singular +`product` object (not an array) for full product detail page rendering. + +#### Request + +{{ extension_schema_fields('catalog_lookup.json#/$defs/get_product_request', 'catalog-mcp') }} + +#### Response + +{{ extension_schema_fields('catalog_lookup.json#/$defs/get_product_response', 'catalog-mcp') }} + +#### Example: With Option Selection + +The user selected Color=Blue on a product with Color and Size options. +The response includes availability signals on option values showing what's +available given that selection. + +=== "Request" + + ```json + { + "jsonrpc": "2.0", + "id": 3, + "method": "tools/call", + "params": { + "name": "get_product", + "arguments": { + "meta": { + "ucp-agent": { + "profile": "https://platform.example/profiles/v2026-01/shopping-agent.json" + } + }, + "catalog": { + "id": "prod_abc123", + "selected": [ + { "name": "Color", "label": "Blue" } + ], + "preferences": ["Color", "Size"], + "context": { + "country": "US" + } + } + } + } + } + ``` + +=== "Response" + + ```json + { + "jsonrpc": "2.0", + "id": 3, + "result": { + "structuredContent": { + "ucp": { + "version": "2026-02-01", + "capabilities": { + "dev.ucp.shopping.catalog.lookup": [ + {"version": "2026-02-01"} + ] + } + }, + "product": { + "id": "prod_abc123", + "handle": "runner-pro", + "title": "Runner Pro", + "description": { + "plain": "Lightweight running shoes with responsive cushioning." + }, + "url": "https://business.example.com/products/runner-pro", + "price_range": { + "min": { "amount": 12000, "currency": "USD" }, + "max": { "amount": 15000, "currency": "USD" } + }, + "media": [ + { + "type": "image", + "url": "https://cdn.example.com/products/runner-pro-blue.jpg", + "alt_text": "Runner Pro in Blue" + } + ], + "options": [ + { + "name": "Color", + "values": [ + {"label": "Blue", "available": true, "exists": true}, + {"label": "Red", "available": true, "exists": true}, + {"label": "Green", "available": false, "exists": true} + ] + }, + { + "name": "Size", + "values": [ + {"label": "8", "available": true, "exists": true}, + {"label": "9", "available": true, "exists": true}, + {"label": "10", "available": true, "exists": true}, + {"label": "11", "available": false, "exists": false}, + {"label": "12", "available": true, "exists": true} + ] + } + ], + "selected": [ + { "name": "Color", "label": "Blue" } + ], + "variants": [ + { + "id": "var_abc123_blue_8", + "sku": "RP-BLU-08", + "title": "Blue / Size 8", + "description": { "plain": "Blue, Size 8" }, + "price": { "amount": 12000, "currency": "USD" }, + "availability": { "available": true }, + "options": [ + { "name": "Color", "label": "Blue" }, + { "name": "Size", "label": "8" } + ] + }, + { + "id": "var_abc123_blue_9", + "sku": "RP-BLU-09", + "title": "Blue / Size 9", + "description": { "plain": "Blue, Size 9" }, + "price": { "amount": 12000, "currency": "USD" }, + "availability": { "available": true }, + "options": [ + { "name": "Color", "label": "Blue" }, + { "name": "Size", "label": "9" } + ] + }, + { + "id": "var_abc123_blue_10", + "sku": "RP-BLU-10", + "title": "Blue / Size 10", + "description": { "plain": "Blue, Size 10" }, + "price": { "amount": 12000, "currency": "USD" }, + "availability": { "available": true }, + "options": [ + { "name": "Color", "label": "Blue" }, + { "name": "Size", "label": "10" } + ] + }, + { + "id": "var_abc123_blue_12", + "sku": "RP-BLU-12", + "title": "Blue / Size 12", + "description": { "plain": "Blue, Size 12" }, + "price": { "amount": 15000, "currency": "USD" }, + "availability": { "available": true }, + "options": [ + { "name": "Color", "label": "Blue" }, + { "name": "Size", "label": "12" } + ] + } + ], + "rating": { + "value": 4.5, + "scale_max": 5, + "count": 128 + } + } + } + } + } + ``` + +In this response: Green is out of stock (`available: false, exists: true`), +Size 11 doesn't exist in Blue (`exists: false`), and four Blue variants are +returned matching the selection. + +#### Product Not Found + +When the identifier does not resolve to a product, return a JSON-RPC error: + +```json +{ + "jsonrpc": "2.0", + "id": 3, + "error": { + "code": -32602, + "message": "Product not found", + "data": { + "id": "prod_invalid" + } + } +} +``` + +Unlike `lookup_catalog` (which returns partial results for batch requests), +`get_product` is a single-resource operation. A missing product is an error, +not a partial success. + ## Error Handling UCP uses a two-layer error model separating transport errors from business outcomes. @@ -453,10 +648,10 @@ not found. "result": { "structuredContent": { "ucp": { - "version": "2026-01-11", + "version": "2026-02-01", "capabilities": { "dev.ucp.shopping.catalog.lookup": [ - {"version": "2026-01-11"} + {"version": "2026-02-01"} ] } }, @@ -482,12 +677,16 @@ results. A conforming MCP transport implementation **MUST**: 1. Implement JSON-RPC 2.0 protocol correctly. -2. Provide both `search_catalog` and `lookup_catalog` tools. +2. Provide `search_catalog`, `lookup_catalog`, and `get_product` tools. 3. Require `catalog.query` parameter for `search_catalog`. 4. Implement `lookup_catalog` per [Catalog Lookup](lookup.md) capability requirements. -5. Use JSON-RPC errors for transport issues; use `messages` array for business outcomes. -6. Return successful result for lookup requests; unknown identifiers result in fewer products returned (MAY include informational `not_found` messages). -7. Validate tool inputs against UCP schemas. -8. Return products with valid `Price` objects (amount + currency). -9. Support cursor-based pagination with default limit of 10. -10. Return `-32602` (Invalid params) for requests exceeding batch size limits. +5. Implement `get_product` per [Catalog Lookup](lookup.md#get-product-get_product) capability requirements. +6. Use JSON-RPC errors for transport issues; use `messages` array for business outcomes. +7. Return successful result for lookup requests; unknown identifiers result in fewer products returned (MAY include informational `not_found` messages). +8. Return JSON-RPC `-32602` error for `get_product` when the identifier is not found. +9. Validate tool inputs against UCP schemas. +10. Return products with valid `Price` objects (amount + currency). +11. Support cursor-based pagination with default limit of 10. +12. Return `-32602` (Invalid params) for requests exceeding batch size limits. +13. Return one featured variant per product for `search_catalog` and `lookup_catalog` when looking up by product ID. When looking up by variant ID, return only the requested variant. +14. When `get_product` includes `selected` options, return `available` and `exists` signals on option values. diff --git a/docs/specification/catalog/rest.md b/docs/specification/catalog/rest.md index e562b322..76e185d8 100644 --- a/docs/specification/catalog/rest.md +++ b/docs/specification/catalog/rest.md @@ -49,7 +49,7 @@ Businesses advertise REST transport availability through their UCP profile at }, { "name": "dev.ucp.shopping.catalog.lookup", - "version": "2026-01-11", + "version": "2026-02-01", "spec": "https://ucp.dev/specification/catalog/lookup", "schema": "https://ucp.dev/schemas/shopping/catalog_lookup.json" } @@ -63,7 +63,8 @@ Businesses advertise REST transport availability through their UCP profile at | Endpoint | Method | Capability | Description | | :--- | :--- | :--- | :--- | | `/catalog/search` | POST | [Search](search.md) | Search for products. | -| `/catalog/lookup` | POST | [Lookup](lookup.md) | Lookup one or more products by ID. | +| `/catalog/lookup` | POST | [Lookup](lookup.md) | Batch lookup products by ID. | +| `/catalog/product` | POST | [Lookup](lookup.md#get-product-get_product) | Get full detail for a single product. | ### `POST /catalog/search` @@ -147,7 +148,7 @@ Maps to the [Catalog Search](search.md) capability. "description": { "plain": "Size 10 variant" }, "price": { "amount": 12000, "currency": "USD" }, "availability": { "available": true }, - "selected_options": [ + "options": [ { "name": "Size", "label": "10" } ], "tags": ["running", "road", "neutral"], @@ -214,11 +215,11 @@ applies to all lookups in the batch. ```json { "ucp": { - "version": "2026-01-11", + "version": "2026-02-01", "capabilities": [ { "name": "dev.ucp.shopping.catalog.lookup", - "version": "2026-01-11" + "version": "2026-02-01" } ] }, @@ -293,11 +294,11 @@ messages indicating which identifiers were not found. ```json { "ucp": { - "version": "2026-01-11", + "version": "2026-02-01", "capabilities": [ { "name": "dev.ucp.shopping.catalog.lookup", - "version": "2026-01-11" + "version": "2026-02-01" } ] }, @@ -329,6 +330,174 @@ messages indicating which identifiers were not found. } ``` +### `POST /catalog/product` + +Maps to the [Catalog Lookup](lookup.md#get-product-get_product) capability. Returns a singular +`product` object (not an array) for full product detail page rendering. + +{{ method_fields('get_product', 'rest.openapi.json', 'catalog-rest') }} + +#### Example: With Option Selection + +The user selected Color=Blue. The response includes availability signals +on option values and returns variants matching the selection. + +=== "Request" + + ```json + POST /catalog/product HTTP/1.1 + Host: business.example.com + Content-Type: application/json + + { + "id": "prod_abc123", + "selected": [ + { "name": "Color", "label": "Blue" } + ], + "preferences": ["Color", "Size"], + "context": { + "country": "US" + } + } + ``` + +=== "Response" + + ```json + { + "ucp": { + "version": "2026-02-01", + "capabilities": [ + { + "name": "dev.ucp.shopping.catalog.lookup", + "version": "2026-02-01" + } + ] + }, + "product": { + "id": "prod_abc123", + "handle": "runner-pro", + "title": "Runner Pro", + "description": { + "plain": "Lightweight running shoes with responsive cushioning." + }, + "url": "https://business.example.com/products/runner-pro", + "price_range": { + "min": { "amount": 12000, "currency": "USD" }, + "max": { "amount": 15000, "currency": "USD" } + }, + "media": [ + { + "type": "image", + "url": "https://cdn.example.com/products/runner-pro-blue.jpg", + "alt_text": "Runner Pro in Blue" + } + ], + "options": [ + { + "name": "Color", + "values": [ + {"label": "Blue", "available": true, "exists": true}, + {"label": "Red", "available": true, "exists": true}, + {"label": "Green", "available": false, "exists": true} + ] + }, + { + "name": "Size", + "values": [ + {"label": "8", "available": true, "exists": true}, + {"label": "9", "available": true, "exists": true}, + {"label": "10", "available": true, "exists": true}, + {"label": "11", "available": false, "exists": false}, + {"label": "12", "available": true, "exists": true} + ] + } + ], + "selected": [ + { "name": "Color", "label": "Blue" } + ], + "variants": [ + { + "id": "var_abc123_blue_8", + "sku": "RP-BLU-08", + "title": "Blue / Size 8", + "description": { "plain": "Blue, Size 8" }, + "price": { "amount": 12000, "currency": "USD" }, + "availability": { "available": true }, + "options": [ + { "name": "Color", "label": "Blue" }, + { "name": "Size", "label": "8" } + ] + }, + { + "id": "var_abc123_blue_9", + "sku": "RP-BLU-09", + "title": "Blue / Size 9", + "description": { "plain": "Blue, Size 9" }, + "price": { "amount": 12000, "currency": "USD" }, + "availability": { "available": true }, + "options": [ + { "name": "Color", "label": "Blue" }, + { "name": "Size", "label": "9" } + ] + }, + { + "id": "var_abc123_blue_10", + "sku": "RP-BLU-10", + "title": "Blue / Size 10", + "description": { "plain": "Blue, Size 10" }, + "price": { "amount": 12000, "currency": "USD" }, + "availability": { "available": true }, + "options": [ + { "name": "Color", "label": "Blue" }, + { "name": "Size", "label": "10" } + ] + }, + { + "id": "var_abc123_blue_12", + "sku": "RP-BLU-12", + "title": "Blue / Size 12", + "description": { "plain": "Blue, Size 12" }, + "price": { "amount": 15000, "currency": "USD" }, + "availability": { "available": true }, + "options": [ + { "name": "Color", "label": "Blue" }, + { "name": "Size", "label": "12" } + ] + } + ], + "rating": { + "value": 4.5, + "scale_max": 5, + "count": 128 + } + } + } + ``` + +Green is out of stock (`available: false, exists: true`). Size 11 doesn't +exist in Blue (`exists: false`). Variants returned match the Color=Blue +selection. + +#### Product Not Found + +When the identifier does not resolve to a product, return HTTP 404: + +```json +HTTP/1.1 404 Not Found +Content-Type: application/json + +{ + "type": "error", + "code": "not_found", + "content": "prod_invalid" +} +``` + +Unlike `/catalog/lookup` (which returns partial results for batch requests), +`/catalog/product` is a single-resource operation. A missing product is a +transport error, not a business outcome. + ## Error Handling UCP uses a two-layer error model separating transport errors from business outcomes. @@ -358,11 +527,11 @@ MAY include informational messages indicating which identifiers were not found. ```json { "ucp": { - "version": "2026-01-11", + "version": "2026-02-01", "capabilities": [ { "name": "dev.ucp.shopping.catalog.lookup", - "version": "2026-01-11" + "version": "2026-02-01" } ] }, @@ -390,7 +559,11 @@ A conforming REST transport implementation **MUST**: 1. Implement the `POST /catalog/search` endpoint with required `query` parameter. 2. Implement the `POST /catalog/lookup` endpoint per [Catalog Lookup](lookup.md) capability requirements. -3. Return products with valid `Price` objects (amount + currency). -4. Support cursor-based pagination with default limit of 10. -5. Return HTTP 200 for lookup requests; unknown identifiers result in fewer products returned (MAY include informational `not_found` messages). -6. Return HTTP 400 with `request_too_large` error for requests exceeding batch size limits. +3. Implement the `POST /catalog/product` endpoint per [Catalog Lookup](lookup.md#get-product-get_product) capability requirements. +4. Return products with valid `Price` objects (amount + currency). +5. Support cursor-based pagination with default limit of 10. +6. Return HTTP 200 for lookup requests; unknown identifiers result in fewer products returned (MAY include informational `not_found` messages). +7. Return HTTP 404 for `POST /catalog/product` when the identifier is not found. +8. Return HTTP 400 with `request_too_large` error for requests exceeding batch size limits. +9. Return one featured variant per product for `POST /catalog/search` and `POST /catalog/lookup` when looking up by product ID. When looking up by variant ID, return only the requested variant. +10. When `POST /catalog/product` includes `selected` options, return `available` and `exists` signals on option values. diff --git a/docs/specification/catalog/search.md b/docs/specification/catalog/search.md index bbc3b949..74efdeb7 100644 --- a/docs/specification/catalog/search.md +++ b/docs/specification/catalog/search.md @@ -46,6 +46,23 @@ merchants MAY support additional custom filters via `additionalProperties`. {{ schema_fields('types/price_filter', 'catalog') }} +## Variant Cardinality + +Businesses SHOULD return **one featured variant per product** in search results. +The featured variant is the most relevant match for the query and context (e.g., +the variant whose title or options best match the search terms). Platforms SHOULD +treat the first variant as featured for display. + +This keeps search fast and efficient for discovery. Use +[`get_product`](lookup.md#get-product-get_product) for variant selection, option +availability, and detailed product context. + +## Result Limits + +Implementations SHOULD accept a page size of at least 10 results per request. +Implementations MAY enforce a maximum page size and MUST reject requests exceeding +their limit with an appropriate error. + ## Pagination Cursor-based pagination for list operations. Cursors are opaque strings diff --git a/source/schemas/shopping/catalog_lookup.json b/source/schemas/shopping/catalog_lookup.json index 73df3c9e..e59e45a7 100644 --- a/source/schemas/shopping/catalog_lookup.json +++ b/source/schemas/shopping/catalog_lookup.json @@ -2,14 +2,14 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://ucp.dev/schemas/shopping/catalog_lookup.json", "name": "dev.ucp.shopping.catalog.lookup", - "version": "2026-01-11", + "version": "2026-02-01", "title": "Catalog Lookup", - "description": "Product/variant lookup by identifier capability.", + "description": "Product/variant lookup by identifier. Supports batch retrieval (lookup_catalog) and single-product detail (get_product).", "type": "object", "$defs": { "lookup_request": { "type": "object", - "description": "Request body for catalog lookup.", + "description": "Request body for batch catalog lookup.", "required": ["ids"], "properties": { "ids": { @@ -23,6 +23,33 @@ } } }, + "lookup_variant": { + "description": "A variant in a lookup response, extended with input correlation.", + "type": "object", + "allOf": [{ "$ref": "types/variant.json" }], + "required": ["input"], + "properties": { + "input": { + "type": "array", + "items": { "$ref": "types/input_correlation.json" }, + "minItems": 1, + "description": "Which request identifiers resolved to this variant, and how." + } + } + }, + "lookup_product": { + "description": "A product in a lookup response, with variants carrying input correlation.", + "type": "object", + "allOf": [{ "$ref": "types/product.json" }], + "properties": { + "variants": { + "type": "array", + "items": { "$ref": "#/$defs/lookup_variant" }, + "minItems": 1, + "description": "Purchasable variants with lookup input correlation. First item is the featured variant." + } + } + }, "lookup_response": { "type": "object", "required": [ @@ -36,7 +63,7 @@ "products": { "type": "array", "items": { - "$ref": "types/product.json" + "$ref": "#/$defs/lookup_product" }, "description": "Products matching the requested identifiers. May contain fewer items if some identifiers not found, or more if identifiers match multiple products." }, @@ -48,6 +75,69 @@ "description": "Errors, warnings, or informational messages about the requested items." } } + }, + "get_product_request": { + "type": "object", + "description": "Request body for single-product retrieval with optional option selection.", + "required": ["id"], + "properties": { + "id": { + "type": "string", + "description": "Product or variant identifier. Implementations MUST support product ID and variant ID." + }, + "selected": { + "type": "array", + "items": { + "$ref": "types/selected_option.json" + }, + "description": "Partial or full option selections for interactive variant narrowing. When provided, response option values include availability signals (available, exists) relative to these selections." + }, + "preferences": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Option names in relaxation priority order. When no exact variant matches all selections, the server drops options from the end of this list first. E.g., ['Color', 'Size'] keeps Color and relaxes Size." + }, + "context": { + "$ref": "types/context.json" + } + } + }, + "detail_product": { + "description": "A product in a get_product response, extended with effective selections.", + "type": "object", + "allOf": [{ "$ref": "types/product.json" }], + "properties": { + "selected": { + "type": "array", + "items": { "$ref": "types/selected_option.json" }, + "description": "Effective option selections after any relaxation. Present when the request includes selected options." + } + } + }, + "get_product_response": { + "type": "object", + "required": [ + "ucp", + "product" + ], + "properties": { + "ucp": { + "$ref": "../ucp.json#/$defs/response_catalog_schema" + }, + "product": { + "$ref": "#/$defs/detail_product", + "description": "The requested product with full detail. Singular — this is a single-resource operation." + }, + "messages": { + "type": "array", + "items": { + "$ref": "types/message.json" + }, + "description": "Warnings or informational messages about the product (e.g., price recently changed, limited availability)." + } + } } } } diff --git a/source/schemas/shopping/types/option_value.json b/source/schemas/shopping/types/option_value.json index f243b631..0ae5c343 100644 --- a/source/schemas/shopping/types/option_value.json +++ b/source/schemas/shopping/types/option_value.json @@ -11,6 +11,14 @@ "label": { "type": "string", "description": "Display text for this option value (e.g., 'Small', 'Blue')." + }, + "available": { + "type": "boolean", + "description": "Whether a variant matching this value and the current option selections is purchasable." + }, + "exists": { + "type": "boolean", + "description": "Whether a variant matching this value and the current option selections exists in the catalog." } } } diff --git a/source/schemas/shopping/types/variant.json b/source/schemas/shopping/types/variant.json index 4d3f4865..4d5dffcf 100644 --- a/source/schemas/shopping/types/variant.json +++ b/source/schemas/shopping/types/variant.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://ucp.dev/schemas/shopping/types/variant.json", "title": "Variant", - "description": "A purchasable variant of a product with specific option selections.", + "description": "A purchasable variant of a product with specific option values.", "type": "object", "required": [ "id", @@ -65,12 +65,12 @@ } } }, - "selected_options": { + "options": { "type": "array", "items": { "$ref": "selected_option.json" }, - "description": "Option selections that define this variant." + "description": "Option values that define this variant (e.g., Color: Blue, Size: Large)." }, "media": { "type": "array", @@ -94,13 +94,6 @@ "type": "object", "description": "Business-defined custom data extending the standard variant model." }, - "input": { - "type": "array", - "items": { - "$ref": "input_correlation.json" - }, - "description": "Lookup correlation: which request identifiers resolved to this variant, and how. Present in lookup responses; absent in search and get_product responses." - }, "seller": { "type": "object", "description": "Optional seller context for this variant.", diff --git a/source/services/shopping/mcp.openrpc.json b/source/services/shopping/mcp.openrpc.json index 31c1ecac..5eaf4bd9 100644 --- a/source/services/shopping/mcp.openrpc.json +++ b/source/services/shopping/mcp.openrpc.json @@ -305,6 +305,27 @@ "name": "response", "schema": {"$ref": "../../schemas/shopping/catalog_lookup.json#/$defs/lookup_response"} } + }, + { + "name": "get_product", + "summary": "Get full details for a single product", + "description": "Retrieve complete product detail by identifier. Returns a singular product with a relevant set of variants, exact pricing, and real-time availability. Supports interactive option selection via `selected` and `preferences` parameters for variant narrowing and availability signals. Use for product detail page rendering and purchase decisions.", + "params": [ + { + "name": "meta", + "required": true, + "schema": {"$ref": "#/components/schemas/meta"} + }, + { + "name": "catalog", + "required": true, + "schema": {"$ref": "../../schemas/shopping/catalog_lookup.json#/$defs/get_product_request"} + } + ], + "result": { + "name": "response", + "schema": {"$ref": "../../schemas/shopping/catalog_lookup.json#/$defs/get_product_response"} + } } ] } diff --git a/source/services/shopping/rest.openapi.json b/source/services/shopping/rest.openapi.json index 51bb7e85..c2901553 100644 --- a/source/services/shopping/rest.openapi.json +++ b/source/services/shopping/rest.openapi.json @@ -666,6 +666,43 @@ } } } + }, + "/catalog/product": { + "post": { + "operationId": "get_product", + "summary": "Get Product Details", + "description": "Retrieve complete product detail by identifier. Returns a singular product with a relevant set of variants, exact pricing, and real-time availability. Supports interactive option selection via `selected` and `preferences` parameters for variant narrowing and availability signals.", + "parameters": [ + { "$ref": "#/components/parameters/authorization" }, + { "$ref": "#/components/parameters/x_api_key" }, + { "$ref": "#/components/parameters/request_signature" }, + { "$ref": "#/components/parameters/request_id" }, + { "$ref": "#/components/parameters/user_agent" }, + { "$ref": "#/components/parameters/ucp_agent" }, + { "$ref": "#/components/parameters/content_type" }, + { "$ref": "#/components/parameters/accept" }, + { "$ref": "#/components/parameters/accept_language" }, + { "$ref": "#/components/parameters/accept_encoding" } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/catalog_get_product_request" } + } + } + }, + "responses": { + "200": { + "description": "Product detail", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/catalog_get_product_response" } + } + } + } + } + } } }, "webhooks": { @@ -919,6 +956,12 @@ }, "catalog_lookup_response": { "$ref": "../../schemas/shopping/catalog_lookup.json#/$defs/lookup_response" + }, + "catalog_get_product_request": { + "$ref": "../../schemas/shopping/catalog_lookup.json#/$defs/get_product_request" + }, + "catalog_get_product_response": { + "$ref": "../../schemas/shopping/catalog_lookup.json#/$defs/get_product_response" } } }