From b991c7dc9c29b915a786a2b8fab1d37e5bada43a Mon Sep 17 00:00:00 2001 From: Jing Li Date: Tue, 2 Jun 2026 23:41:50 -0400 Subject: [PATCH 01/37] Extend retail_location.json with more fields and move it out of shopping/ to generally represent any physical location (to be referenced by Location capability). --- .cspell/custom-words.txt | 14 +++++ source/schemas/common/types/daily_hour.json | 29 +++++++++++ .../schemas/common/types/exception_hour.json | 34 +++++++++++++ source/schemas/common/types/geo.json | 26 ++++++++++ source/schemas/common/types/location.json | 51 +++++++++++++++++++ .../schemas/common/types/time_interval.json | 20 ++++++++ .../types/fulfillment_destination.json | 2 +- .../shopping/types/retail_location.json | 25 --------- 8 files changed, 175 insertions(+), 26 deletions(-) create mode 100644 source/schemas/common/types/daily_hour.json create mode 100644 source/schemas/common/types/exception_hour.json create mode 100644 source/schemas/common/types/geo.json create mode 100644 source/schemas/common/types/location.json create mode 100644 source/schemas/common/types/time_interval.json delete mode 100644 source/schemas/shopping/types/retail_location.json diff --git a/.cspell/custom-words.txt b/.cspell/custom-words.txt index 00701fc6a..53c9c767f 100644 --- a/.cspell/custom-words.txt +++ b/.cspell/custom-words.txt @@ -155,3 +155,17 @@ punycode userinfo examplecorp zapatillas +jwks +keyid +reauth +redeemables +reprepare +sandboxing +fbclid +gclid +ttclid +Accor +Kogan +Petbarn +VTEX +geofence diff --git a/source/schemas/common/types/daily_hour.json b/source/schemas/common/types/daily_hour.json new file mode 100644 index 000000000..43f241728 --- /dev/null +++ b/source/schemas/common/types/daily_hour.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/types/daily_hours.json", + "title": "Daily Hours", + "description": "Operating hours for a specific day of the week.", + "type": "object", + "required": ["day"], + "properties": { + "day": { + "type": "string", + "enum": ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"] + }, + "is_closed": { + "type": "boolean", + "description": "If true, the location is closed on this day. When true, intervals MUST be omitted." + }, + "is_24_hours": { + "type": "boolean", + "description": "If true, open 24 hours on this day. When true, intervals MUST be omitted." + }, + "intervals": { + "type": "array", + "description": "One or more open intervals for this day. Supports split shifts.", + "items": { + "$ref": "time_interval.json" + } + } + } +} diff --git a/source/schemas/common/types/exception_hour.json b/source/schemas/common/types/exception_hour.json new file mode 100644 index 000000000..2bfb1cb38 --- /dev/null +++ b/source/schemas/common/types/exception_hour.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/types/exception_hours.json", + "title": "Exception Hours", + "description": "Operating hours for a specific date (e.g., holiday or temporary change).", + "type": "object", + "required": ["date"], + "properties": { + "date": { + "type": "string", + "format": "date", + "description": "An ISO 8601 date." + }, + "label": { + "type": "string", + "description": "Human readable explanation for the exception (e.g., 'Thanksgiving')." + }, + "is_closed": { + "type": "boolean", + "description": "If true, the location is closed on this date. When true, intervals MUST be omitted." + }, + "is_24_hours": { + "type": "boolean", + "description": "If true, open 24 hours on this date. When true, intervals MUST be omitted." + }, + "intervals": { + "type": "array", + "description": "One or more open intervals for this date.", + "items": { + "$ref": "time_interval.json" + } + } + } +} diff --git a/source/schemas/common/types/geo.json b/source/schemas/common/types/geo.json new file mode 100644 index 000000000..75d84fca5 --- /dev/null +++ b/source/schemas/common/types/geo.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/types/geo.json", + "title": "Geo Representation", + "description": "Geographic information.", + "type": "object", + "required": ["latitude", "longitude"], + "properties": { + "latitude": { + "type": "number", + "minimum": -90, + "maximum": 90, + "description": "Latitude in decimal degrees." + }, + "longitude": { + "type": "number", + "minimum": -180, + "maximum": 180, + "description": "Longitude in decimal degrees." + }, + "geofence_radius": { + "type": "number", + "description": "Geofence radius in meters. Used for proximity detection." + } + } +} diff --git a/source/schemas/common/types/location.json b/source/schemas/common/types/location.json new file mode 100644 index 000000000..704b3b6b9 --- /dev/null +++ b/source/schemas/common/types/location.json @@ -0,0 +1,51 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/types/location.json", + "title": "Location", + "description": "A physical location (e.g., store, restaurant, locker, warehouse).", + "type": "object", + "required": ["id", "name"], + "additionalProperties": true, + "properties": { + "id": { + "type": "string", + "description": "Unique location identifier." + }, + "name": { + "type": "string", + "description": "Location display name.", + "ucp_request": "omit" + }, + "address": { + "$ref": "postal_address.json", + "description": "Physical address of the location.", + "ucp_request": "omit" + }, + "geo": { + "$ref": "geo.json", + "description": "Geographic coordinates and geofence for the location.", + "ucp_request": "omit" + }, + "hours": { + "type": "array", + "description": "Regular weekly operating hours. For overnight hours, use two entries across two days.", + "items": { + "$ref": "daily_hour.json" + }, + "ucp_request": "omit" + }, + "exception_hours": { + "type": "array", + "description": "Exception hours for specific dates (holidays, closures, etc.).", + "items": { + "$ref": "exception_hour.json" + }, + "ucp_request": "omit" + }, + "timezone": { + "type": "string", + "description": "IANA timezone identifier (e.g., 'America/New_York'). MUST be set when hours or exception_hours are present Required for correct interpretation.", + "ucp_request": "omit" + } + } +} diff --git a/source/schemas/common/types/time_interval.json b/source/schemas/common/types/time_interval.json new file mode 100644 index 000000000..c9d2d568b --- /dev/null +++ b/source/schemas/common/types/time_interval.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/types/time_interval.json", + "title": "Time Interval", + "description": "An open time interval with 24-hour HH:MM format.", + "type": "object", + "required": ["open", "close"], + "properties": { + "open": { + "type": "string", + "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$", + "description": "Start time (e.g., '09:00')." + }, + "close": { + "type": "string", + "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$", + "description": "End time (e.g., '18:00')." + } + } +} diff --git a/source/schemas/shopping/types/fulfillment_destination.json b/source/schemas/shopping/types/fulfillment_destination.json index cc6708dc9..8ae54579c 100644 --- a/source/schemas/shopping/types/fulfillment_destination.json +++ b/source/schemas/shopping/types/fulfillment_destination.json @@ -10,7 +10,7 @@ "$ref": "shipping_destination.json" }, { - "$ref": "retail_location.json" + "$ref": "../../common/types/location.json" } ] } diff --git a/source/schemas/shopping/types/retail_location.json b/source/schemas/shopping/types/retail_location.json deleted file mode 100644 index 2e9d5d88f..000000000 --- a/source/schemas/shopping/types/retail_location.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://ucp.dev/schemas/shopping/types/retail_location.json", - "title": "Retail Location", - "description": "A pickup location (retail store, locker, etc.).", - "type": "object", - "ucp_shared_request": true, - "required": ["id", "name"], - "additionalProperties": true, - "properties": { - "id": { - "type": "string", - "description": "Unique location identifier.", - "ucp_request": "omit" - }, - "name": { - "type": "string", - "description": "Location name (e.g., store name)." - }, - "address": { - "$ref": "../../common/types/postal_address.json", - "description": "Physical address of the location." - } - } -} From c5ad8d2100ddf37016a8d607e9876c83cfdfe6f6 Mon Sep 17 00:00:00 2001 From: Jing Li Date: Wed, 3 Jun 2026 00:01:12 -0400 Subject: [PATCH 02/37] Add appropriate transition annotation to set proper expectation on field presence. --- source/schemas/common/types/location.json | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/source/schemas/common/types/location.json b/source/schemas/common/types/location.json index 704b3b6b9..f29fe2e49 100644 --- a/source/schemas/common/types/location.json +++ b/source/schemas/common/types/location.json @@ -9,17 +9,29 @@ "properties": { "id": { "type": "string", - "description": "Unique location identifier." + "description": "Unique location identifier.", + "ucp_request": { + "transition": { + "from": "omit", + "to": "optional", + "description": "Location ids MAY be specified by platforms in requests." + } + } }, "name": { "type": "string", "description": "Location display name.", - "ucp_request": "omit" + "ucp_request": { + "transition": { + "from": "required", + "to": "omit", + "description": "Location names should not be specified by platforms." + } + } }, "address": { "$ref": "postal_address.json", - "description": "Physical address of the location.", - "ucp_request": "omit" + "description": "Physical address of the location." }, "geo": { "$ref": "geo.json", From 525843ea195a2d1173009646ee98f9e10d17d653 Mon Sep 17 00:00:00 2001 From: Jing Li Date: Wed, 24 Jun 2026 18:08:00 -0400 Subject: [PATCH 03/37] Add common location capability. --- .cspell/custom-words.txt | 3 + docs/specification/location/index.md | 147 +++++ docs/specification/location/lookup.md | 76 +++ docs/specification/location/mcp.md | 333 +++++++++++ docs/specification/location/rest.md | 533 ++++++++++++++++++ docs/specification/location/search.md | 123 ++++ mkdocs.yml | 25 + source/schemas/common/location_lookup.json | 59 ++ source/schemas/common/location_search.json | 60 ++ .../{shopping => common}/types/context.json | 2 +- source/schemas/common/types/geo.json | 2 +- .../schemas/common/types/location_filter.json | 58 ++ .../types/location_offering_filter.json | 48 ++ .../{shopping => common}/types/signals.json | 0 source/schemas/shopping/cart.json | 4 +- source/schemas/shopping/catalog_lookup.json | 8 +- source/schemas/shopping/catalog_search.json | 4 +- source/schemas/shopping/checkout.json | 4 +- .../types/fulfillment_destination.json | 3 +- source/services/common/mcp.openrpc.json | 97 ++++ source/services/common/rest.openapi.json | 259 +++++++++ 21 files changed, 1835 insertions(+), 13 deletions(-) create mode 100644 docs/specification/location/index.md create mode 100644 docs/specification/location/lookup.md create mode 100644 docs/specification/location/mcp.md create mode 100644 docs/specification/location/rest.md create mode 100644 docs/specification/location/search.md create mode 100644 source/schemas/common/location_lookup.json create mode 100644 source/schemas/common/location_search.json rename source/schemas/{shopping => common}/types/context.json (97%) create mode 100644 source/schemas/common/types/location_filter.json create mode 100644 source/schemas/common/types/location_offering_filter.json rename source/schemas/{shopping => common}/types/signals.json (100%) create mode 100644 source/services/common/mcp.openrpc.json create mode 100644 source/services/common/rest.openapi.json diff --git a/.cspell/custom-words.txt b/.cspell/custom-words.txt index 53c9c767f..f402ae457 100644 --- a/.cspell/custom-words.txt +++ b/.cspell/custom-words.txt @@ -169,3 +169,6 @@ Kogan Petbarn VTEX geofence +geofencing +wifi +optionalities diff --git a/docs/specification/location/index.md b/docs/specification/location/index.md new file mode 100644 index 000000000..75bb84cf7 --- /dev/null +++ b/docs/specification/location/index.md @@ -0,0 +1,147 @@ + + +# Location Capability + +The Location capability allows platforms to discover, search, and retrieve physical locations +(such as retail stores, restaurants, brand lockers) from businesses. + +This is vertical-agnostic and enables key commerce flows such as: + +* **Local Pickup Discovery**: Finding locations like retail stores or restaurant branches + nearby that support customer pickup and checking their operating hours & inventory availability + before selection. +* **Fulfillment Area Verification**: Checking if a specific location (e.g., utility depot, restaurant, + or local service provider) has delivery coverage for a buyer's address. + +## Capabilities + +| Capability | Description | +| :--- | :--- | +| [`dev.ucp.common.location.search`](search.md) | Search for locations using natural language queries and filters (hours, offerings, geofencing). | +| [`dev.ucp.common.location.lookup`](lookup.md) | Retrieve full details for one or more locations by identifier. | + +## Key Concepts + +* **Location**: A physical entity that can be found on a map. Defined by a display name, + address, operating hours, and **geographic context** (geographic coordinates and an + optional circular **geofence service radius** for delivery/service area checks). +* **Offerings**: Features, capabilities, and inventory provided by the location. + This is split into two distinct concepts to ensure tooling compatibility and semantic clarity: + * **Amenities**: Static features, services, or capabilities of the location + (e.g., `free_wifi`, `parking`, `outdoor_seating`, `curbside_pickup`). + * **Inventory**: Dynamic availability of goods (e.g., retail products or restaurant dishes). +* **Geofencing**: Locations can define a `geofence_radius` around their coordinates. + This is used to determine if a location can serve a specific user (e.g., delivery area check). + Clients can perform proximity searches (`distance` filter) or coverage checks (`geofence` filter) using the filter. +* **Operating Hours**: Weekly schedules (`hours`) and date-specific overrides + (`exception_hours` - e.g., holidays, temporary closures) associated with a timezone. + +### Relationship to Other Capabilities + +The Location capability provides the foundation for localized commerce by integrating tightly +other capabilities (like Catalog, Cart, and Checkout in Shopping): + +1. **Stable Identifiers**: Location search/lookup operations return stable, + business-scoped `location.id` values. These IDs are referenced further in other requests & responses + (e.g., associating product variants to specific locations in Catalog filters, passed directly + in `selected_destination_id` to indicate pickup fulfillment mode). +2. **Inventory-Based Store Finder**: Platforms can use Location Search with the `offerings.inventory` filter + to locate nearby stores that have a specific item available, bridging the gap between online catalog + browsing and physical store visits. +3. **Provisional vs. Authoritative Boundaries**: + * *Discovery Phase (Provisional)*: Location responses based on operating hours, amenity support, + real-time product stock, property-level amenities represent the business's *current terms* at the + time of query. They are **provisional signals** and are not binding commitments. + * *Checkout Phase (Authoritative)*: Final transaction terms that depend on a location (e.g., pickup) + **MUST** be negotiated and finalized authoritatively. Discovery signals **SHOULD NOT** be cached + or reused across sessions without re-validation. + +## Shared Entities + +### Context + +User location and market context for the operations. All fields are optional +hints for relevance and localization. Platforms **MAY** geo-detect context from +request headers. + +Context signals are provisional—not authoritative data. Businesses **SHOULD** use +these values when verified inputs (e.g., coordinates as part of the request filter) +are absent, and **MAY** ignore or down-rank them if inconsistent with +higher-confidence signals (authenticated account, risk detection). + +{{ schema_fields('types/context', 'location') }} + +### Signals + +Environment data provided by the platform to support authorization +and abuse prevention. Signal values **MUST NOT** be buyer-asserted claims. See +[Signals](../overview.md#signals) for details and privacy requirements. + +{{ schema_fields('types/signals', 'location') }} + +## Messages and Error Handling + +All location responses include an optional `messages` array that allows businesses +to provide context about errors, warnings, or informational notices. + +### Message Types + +Messages communicate business outcomes and provide context: + +| Type | When to Use | Example Codes | +| :--- | :--- | :--- | +| `error` | Business-level errors | `no_service_coverage` (geographic coordinates based filter) | +| `warning` | Important conditions affecting purchase | `permanently_closed`, `temporary_closure` | +| `info` | Additional context without issues | `not_found`, `holiday_hours_active` | + +**Note**: Most catalog errors use `severity: "recoverable"` - agents +handle them programmatically (retry, inform user, show alternatives). + +#### Message (Error) + +{{ schema_fields('types/message_error', 'catalog') }} + +#### Message (Warning) + +{{ schema_fields('types/message_warning', 'catalog') }} + +#### Message (Info) + +{{ schema_fields('types/message_info', 'catalog') }} + +### Common Scenarios + +#### Empty Search + +When search finds no matches, return an empty array without messages. + + +```json +{ + "ucp": {...}, + "locations": [] +} +``` + +This is not an error - the query was valid but returned no results. + +## Transport Bindings + +The capabilities above are bound to specific transport protocols: + +* [REST Binding](rest.md): RESTful API mapping. +* [MCP Binding](mcp.md): Model Context Protocol mapping via JSON-RPC. diff --git a/docs/specification/location/lookup.md b/docs/specification/location/lookup.md new file mode 100644 index 000000000..59e4a9deb --- /dev/null +++ b/docs/specification/location/lookup.md @@ -0,0 +1,76 @@ + + +# Location Lookup Capability + +* **Capability Name:** `dev.ucp.common.location.lookup` + +Retrieves physical locations by their unique identifiers. +Supports full-detail batch retrieval of multiple locations to provide optionalities +or retrieval of a single location (useful for a dedicated location detail page). + +## Operation + +| Operation | Description | +| :--------------------- | :-------------------------------------------- | +| **Lookup Location(s)** | Retrieve single or multiple locations by ID. | + +## Supported Identifiers + +The `ids` parameter accepts an array of identifiers. Implementations **MUST** +support lookup by the business's stable location ID. + +Duplicate identifiers in the request **MUST** be deduplicated by the server. +When multiple identifiers resolve to the same physical location, +it **MUST** be returned only once in the response. + +### Client Correlation + +The response does not guarantee order. Clients correlate returned locations +simply by matching the returned `id` field against their requested `ids`. + +### Batch Size + +Implementations **SHOULD** accept at least 10 identifiers per request. +Implementations **MAY** enforce a maximum batch size and **MUST** reject +requests exceeding their limit with an appropriate error (HTTP 400 +`request_too_large` for REST, JSON-RPC `-32602` for MCP). + +### Filters + +Optional `filters` (hours, offerings/inventory, geo) are accepted +to narrow down the returned locations. +Filters use the same schema and AND semantics as [Search Filters](search.md#search-filters). + +Filters apply **after** identifier resolution. For example, if a client requests +`["loc_downtown", "loc_uptown"]` with a filter of `hours.open_now: true`: + +1. The server first resolves both identifiers to their respective locations. +2. The server then evaluates the `open_now` filter against each resolved location. +3. If `loc_uptown` is currently closed, it is excluded, and only `loc_downtown` is returned. + +### Request + +{{ extension_schema_fields('location_lookup.json#/$defs/lookup_request', 'location') }} + +### Response + +{{ extension_schema_fields('location_lookup.json#/$defs/lookup_response', 'location') }} + +## Transport Bindings + +* [REST Binding](rest.md#post-locationslookup): `POST /locations/lookup` +* [MCP Binding](mcp.md#lookup_locations): `lookup_locations` tool diff --git a/docs/specification/location/mcp.md b/docs/specification/location/mcp.md new file mode 100644 index 000000000..5eb2e5d41 --- /dev/null +++ b/docs/specification/location/mcp.md @@ -0,0 +1,333 @@ + + +# Location - MCP Binding + +This document specifies the Model Context Protocol (MCP) binding for the [Location Capability](index.md). + +## Protocol Fundamentals + +### Discovery + +Businesses advertise MCP transport availability for the Common service and Location capabilities through their UCP profile at `/.well-known/ucp`. + + +```json +{ + "ucp": { + "version": "{{ ucp_version }}", + "services": { + "dev.ucp.common": [ + { + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/overview", + "transport": "mcp", + "schema": "https://ucp.dev/{{ ucp_version }}/services/common/mcp.openrpc.json", + "endpoint": "https://business.example.com/ucp/mcp" + } + ] + }, + "capabilities": { + "dev.ucp.common.location.search": [{ + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/location/search", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/common/location_search.json" + }], + "dev.ucp.common.location.lookup": [{ + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/location/lookup", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/common/location_lookup.json" + }] + }, + "payment_handlers": {} + } +} +``` + +### Request Metadata + +MCP clients **MUST** include a `meta` object in every request containing +protocol metadata: + + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": { + "name": "search_locations", + "arguments": { + "meta": { + "ucp-agent": { + "profile": "https://platform.example/profiles/v2026-01/agent.json" + } + }, + "location": { + "query": "grocery store open now", + "filters": { + "hours": { + "open_now": true + } + } + } + } + } +} +``` + +The `meta["ucp-agent"]` field is **required** on all requests to enable +version compatibility checking and capability negotiation. + +## Tools + +| Tool | Capability | Description | +| :--- | :--- | :--- | +| `search_locations` | [Search](search.md) | Search for locations using text, coordinates, and filters. | +| `lookup_locations` | [Lookup](lookup.md) | Batch lookup one or multiple location(s) by ID. | + +### `search_locations` + +Maps to the [Location Search](search.md) capability. + +#### Request Arguments + +{{ extension_schema_fields('location_search.json#/$defs/search_request', 'location/mcp') }} + +#### Response Schema + +{{ extension_schema_fields('location_search.json#/$defs/search_response', 'location/mcp') }} + +#### Example + +=== "Request" + + + ```json + { + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": { + "name": "search_locations", + "arguments": { + "meta": { + "ucp-agent": { + "profile": "https://platform.example/profiles/v2026-01/agent.json" + } + }, + "location": { + "query": "grocery store near me", + "context": { + "address_country": "US", + "address_region": "CA", + "postal_code": "94043" + }, + "filters": { + "hours": { + "open_now": true + }, + "offerings": { + "amenities": ["curbside_pickup"] + }, + "geo": { + "geofence_point": { + "latitude": 37.422, + "longitude": -122.084 + } + } + } + } + } + } + } + ``` + +=== "Response" + + + ```json + { + "jsonrpc": "2.0", + "id": 1, + "result": { + "structuredContent": { + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.common.location.search": [ + {"version": "{{ ucp_version }}"} + ] + } + }, + "locations": [ + { + "id": "loc_valley_grocers", + "name": "Valley Grocers", + "address": { + "street_address": "789 Maple Ave", + "address_locality": "Mountain View", + "address_region": "CA", + "address_country": "US", + "postal_code": "94043" + }, + "geo": { + "latitude": 37.420, + "longitude": -122.080, + "geofence_radius": 10000 + }, + "timezone": "America/Los_Angeles" + } + ] + } + } + } + ``` + +### `lookup_locations` + +Maps to the [Location Lookup](lookup.md) capability. + +#### Request Arguments + +{{ extension_schema_fields('location_lookup.json#/$defs/lookup_request', 'location/mcp') }} + +#### Response Schema + +{{ extension_schema_fields('location_lookup.json#/$defs/lookup_response', 'location/mcp') }} + +#### Example + +=== "Request" + + + ```json + { + "jsonrpc": "2.0", + "id": 2, + "method": "tools/call", + "params": { + "name": "lookup_locations", + "arguments": { + "meta": { + "ucp-agent": { + "profile": "https://platform.example/profiles/v2026-01/agent.json" + } + }, + "location": { + "ids": ["loc_downtown", "loc_uptown"] + } + } + } + } + ``` + +=== "Response" + + + ```json + { + "jsonrpc": "2.0", + "id": 2, + "result": { + "structuredContent": { + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.common.location.lookup": [ + {"version": "{{ ucp_version }}"} + ] + } + }, + "locations": [ + { + "id": "loc_downtown", + "name": "Downtown Store", + "address": { + "street_address": "100 Broadway", + "address_locality": "New York", + "address_region": "NY", + "address_country": "US", + "postal_code": "10005" + }, + "geo": { + "latitude": 40.707, + "longitude": -74.011, + "geofence_radius": 2000 + }, + "timezone": "America/New_York" + } + ], + "messages": [ + { + "type": "info", + "code": "not_found", + "content": "Unable to find the location associated with loc_uptown" + } + ] + } + } + } + ``` + +## Error Handling + +UCP uses a two-layer error model separating transport-level errors from business outcomes. + +### Transport Errors + +Transport-level failures (authentication, rate limiting, invalid parameters) that prevent request processing are returned as JSON-RPC `error`. See the [Core Specification](../overview.md#error-codes) for details. + +### Business Outcomes + +All application-level outcomes return a successful JSON-RPC result with the UCP envelope and optional `messages` array. See [Location Overview](index.md#messages-and-error-handling) for message semantics. + +## Entities + +### Location {: #location-entity } + +{{ schema_fields('types/location', 'location/mcp') }} + +### Location Filter {: #location-filter-schema } + +{{ schema_fields('types/location_filter', 'location/mcp') }} + +### Location Offering Filter {: #location-offering-filter-schema } + +{{ schema_fields('types/location_offering_filter', 'location/mcp') }} + +### Location Get Result {: #location-get-result-schema } + +{{ schema_fields('services/common/mcp.openrpc.json#/components/schemas/location_get_result', 'location/mcp') }} + +### Error Response {: #error-response } + +{{ schema_fields('types/error_response', 'location/mcp') }} + +## Conformance + +A conforming MCP transport implementation **MUST**: + +1. Implement JSON-RPC 2.0 protocol correctly. +2. Implement tools for each location capability advertised in the business's UCP profile, per their respective + capability requirements ([Search](search.md), [Lookup](lookup.md)). + Each capability may be adopted independently. +3. Default to business-derived coordinates based on user location hint provided in `context.json` + for proximity (`distance`) filters when explicit coordinates are omitted by the platform in the request. +4. Return a successful JSON-RPC result for lookup requests; unknown identifiers result in fewer or no locations + returned (**MAY** include informational `not_found` messages in the `messages` array). +5. Validate tool inputs against UCP schemas. +6. Return `-32602` (Invalid params) for requests exceeding batch size limits. diff --git a/docs/specification/location/rest.md b/docs/specification/location/rest.md new file mode 100644 index 000000000..34ba26927 --- /dev/null +++ b/docs/specification/location/rest.md @@ -0,0 +1,533 @@ + + +# Location - REST Binding + +This document specifies the HTTP/REST binding for the [Location Capability](index.md). + +## Protocol Fundamentals + +### Discovery + +Businesses advertise REST transport availability for the Common service and +Location capabilities through their UCP profile at `/.well-known/ucp`. + + +```json +{ + "ucp": { + "version": "{{ ucp_version }}", + "services": { + "dev.ucp.common": [ + { + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/overview", + "transport": "rest", + "schema": "https://ucp.dev/{{ ucp_version }}/services/common/rest.openapi.json", + "endpoint": "https://business.example.com/ucp" + } + ] + }, + "capabilities": { + "dev.ucp.common.location.search": [{ + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/location/search", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/common/location_search.json" + }], + "dev.ucp.common.location.lookup": [{ + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/location/lookup", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/common/location_lookup.json" + }] + }, + "payment_handlers": {} + } +} +``` + +## Endpoints + +| Endpoint | Method | Capability | Description | +| :--- | :--- | :--- | :--- | +| `/locations/search` | POST | [Search](search.md) | Search for physical locations. | +| `/locations/lookup` | POST | [Lookup](lookup.md) | Lookup single or multiple location(s) by known ID. | + +### `POST /locations/search` + +Maps to the [Location Search](search.md) capability. + +{{ method_fields('search_locations', 'rest.openapi.json', 'location/rest') }} + +#### Example: Search for Grocery Stores with Local Delivery Coverage (Geofencing) + +=== "Request" + + + ```json + POST /locations/search HTTP/1.1 + Host: business.example.com + Content-Type: application/json + Request-Id: 8ef9b0c2-78d1-4e4b-91c2-3e2ef0d3ab9f + UCP-Agent: profile="https://platform.example/profiles/v2026-01/agent.json" + + { + "query": "grocery store near me", + "context": { + "address_country": "US", + "address_region": "CA", + "postal_code": "94043" + }, + "filters": { + "hours": { + "open_now": true + }, + "offerings": { + "amenities": ["curbside_pickup"] + }, + "geo": { + "geofence_point": { + "latitude": 37.422, + "longitude": -122.084 + } + } + } + } + ``` + +=== "Response" + + + ```json + HTTP/1.1 200 OK + Content-Type: application/json + Content-Digest: sha-256=:yG9a8bC7...: + + { + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.common.location.search": [ + {"version": "{{ ucp_version }}"} + ] + } + }, + "locations": [ + { + "id": "loc_valley_grocers", + "name": "Valley Grocers", + "address": { + "street_address": "789 Maple Ave", + "address_locality": "Mountain View", + "address_region": "CA", + "address_country": "US", + "postal_code": "94043" + }, + "geo": { + "latitude": 37.420, + "longitude": -122.080, + "geofence_radius": 10000 + }, + "timezone": "America/Los_Angeles" + } + ] + } + ``` + +#### Example: Search for Electronics Stores with iPhone in stock (Store Finder) + +=== "Request" + + + ```json + POST /locations/search HTTP/1.1 + Host: business.example.com + Content-Type: application/json + Request-Id: 9ef9b0c2-78d1-4e4b-91c2-3e2ef0d3ab9f + UCP-Agent: profile="https://platform.example/profiles/v2026-01/agent.json" + + { + "context": { + "address_country": "US" + }, + "filters": { + "hours": { + "open_now": true + }, + "offerings": { + "inventory": [ + { + "id": "item_id_iphone_15_pro", + "type": "product", + "quantity": 1 + } + ] + }, + "geo": { + "distance": { + "center": { + "latitude": 40.707, + "longitude": -74.011 + }, + "max_distance": 10000 + } + } + } + } + ``` + +=== "Response" + + + ```json + HTTP/1.1 200 OK + Content-Type: application/json + + { + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.common.location.search": [ + {"version": "{{ ucp_version }}"} + ] + } + }, + "locations": [ + { + "id": "loc_downtown_electronics", + "name": "Downtown Electronics", + "address": { + "street_address": "100 Broadway", + "address_locality": "New York", + "address_region": "NY", + "address_country": "US", + "postal_code": "10005" + }, + "geo": { + "latitude": 40.709, + "longitude": -74.008, + "geofence_radius": 2000 + }, + "timezone": "America/New_York" + } + ] + } + ``` + +### `POST /locations/lookup` + +Maps to the [Location Lookup](lookup.md) capability. + +{{ method_fields('lookup_locations', 'rest.openapi.json', 'location/rest') }} + +#### Example: Simple Lookup + +=== "Request" + + + ```json + POST /locations/lookup HTTP/1.1 + Host: business.example.com + Content-Type: application/json + Request-Id: 2c9b0c2a-18d1-4e4b-91c2-3e2ef0d3ab9f + UCP-Agent: profile="https://platform.example/profiles/v2026-01/agent.json" + + { + "ids": ["loc_downtown", "loc_uptown"] + } + ``` + +=== "Response" + + + ```json + HTTP/1.1 200 OK + Content-Type: application/json + + { + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.common.location.lookup": [ + {"version": "{{ ucp_version }}"} + ] + } + }, + "locations": [ + { + "id": "loc_downtown", + "name": "Downtown Store", + "address": { + "street_address": "100 Broadway", + "address_locality": "New York", + "address_region": "NY", + "address_country": "US", + "postal_code": "10005" + }, + "geo": { + "latitude": 40.707, + "longitude": -74.011, + "geofence_radius": 2000 + }, + "timezone": "America/New_York", + "hours": [ + { + "day": "monday", + "intervals": [{"open": "09:00", "close": "21:00"}] + }, + { + "day": "tuesday", + "intervals": [{"open": "09:00", "close": "21:00"}] + }, + { + "day": "wednesday", + "intervals": [{"open": "09:00", "close": "21:00"}] + }, + { + "day": "thursday", + "intervals": [{"open": "09:00", "close": "21:00"}] + }, + { + "day": "friday", + "intervals": [{"open": "09:00", "close": "22:00"}] + }, + { + "day": "saturday", + "intervals": [{"open": "10:00", "close": "20:00"}] + }, + { + "day": "sunday", + "is_closed": true + } + ], + "exception_hours": [ + { + "date": "2026-11-26", + "label": "Thanksgiving", + "is_closed": true + } + ] + }, + { + "id": "loc_uptown", + "name": "Uptown Boutique", + "address": { + "street_address": "2000 Madison Ave", + "address_locality": "New York", + "address_region": "NY", + "address_country": "US", + "postal_code": "10035" + }, + "geo": { + "latitude": 40.790, + "longitude": -73.950, + "geofence_radius": 1000 + }, + "timezone": "America/New_York", + "hours": [ + { + "day": "monday", + "intervals": [{"open": "09:00", "close": "21:00"}] + }, + { + "day": "tuesday", + "intervals": [{"open": "09:00", "close": "21:00"}] + }, + { + "day": "wednesday", + "intervals": [{"open": "09:00", "close": "21:00"}] + }, + { + "day": "thursday", + "intervals": [{"open": "09:00", "close": "21:00"}] + }, + { + "day": "friday", + "intervals": [{"open": "09:00", "close": "22:00"}] + }, + { + "day": "saturday", + "is_closed": true + }, + { + "day": "sunday", + "is_closed": true + } + ], + "exception_hours": [ + { + "date": "2026-11-26", + "label": "Thanksgiving", + "is_closed": true + } + ] + } + ] + } + ``` + +#### Example: Partial Success (Some Locations Not Found) + +=== "Request" + + + ```json + POST /locations/lookup HTTP/1.1 + Host: business.example.com + Content-Type: application/json + Request-Id: 2c9b0c2a-18d1-4e4b-91c2-3e2ef0d3ab9f + UCP-Agent: profile="https://platform.example/profiles/v2026-01/agent.json" + + { + "ids": ["loc_downtown", "loc_invalid_id"] + } + ``` + +=== "Response" + + + ```json + HTTP/1.1 200 OK + Content-Type: application/json + + { + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.common.location.lookup": [ + {"version": "{{ ucp_version }}"} + ] + } + }, + "locations": [ + { + "id": "loc_downtown", + "name": "Downtown Store", + "address": { + "street_address": "100 Broadway", + "address_locality": "New York", + "address_region": "NY", + "address_country": "US", + "postal_code": "10005" + }, + "geo": { + "latitude": 40.707, + "longitude": -74.011, + "geofence_radius": 2000 + }, + "timezone": "America/New_York", + "hours": [ + { + "day": "monday", + "intervals": [{"open": "09:00", "close": "21:00"}] + }, + { + "day": "tuesday", + "intervals": [{"open": "09:00", "close": "21:00"}] + }, + { + "day": "wednesday", + "intervals": [{"open": "09:00", "close": "21:00"}] + }, + { + "day": "thursday", + "intervals": [{"open": "09:00", "close": "21:00"}] + }, + { + "day": "friday", + "intervals": [{"open": "09:00", "close": "22:00"}] + }, + { + "day": "saturday", + "intervals": [{"open": "10:00", "close": "20:00"}] + }, + { + "day": "sunday", + "is_closed": true + } + ], + "exception_hours": [ + { + "date": "2026-11-26", + "label": "Thanksgiving", + "is_closed": true + } + ] + } + ], + "messages": [ + { + "type": "info", + "code": "not_found", + "content": "Unable to find the location associated with loc_invalid_id." + } + ] + } + ``` + +## Error Handling + +UCP uses a two-layer error model separating transport-level errors from business outcomes. + +### Transport Errors + +Use HTTP status codes for protocol-level issues that prevent request processing: + +| Status | Meaning | +| :--- | :--- | +| 400 | Bad Request - Malformed JSON or missing required parameters | +| 401 | Unauthorized - Missing or invalid authentication | +| 429 | Too Many Requests - Rate limited | +| 500 | Internal Server Error | + +### Business Outcomes + +All application-level outcomes return HTTP 200 with the UCP envelope and optional `messages` array. See [Location Overview](index.md#messages-and-error-handling) for message semantics. + +## Entities + +### UCP Response Catalog (Envelope) {: #ucp-response-catalog-schema } + +{{ extension_schema_fields('ucp.json#/$defs/response_catalog_schema', 'location/rest') }} + +### Location {: #location-entity } + +{{ schema_fields('types/location', 'location/rest') }} + +### Location Filter {: #location-filter-schema } + +{{ schema_fields('types/location_filter', 'location/rest') }} + +### Location Offering Filter {: #location-offering-filter-schema } + +{{ schema_fields('types/location_offering_filter', 'location/rest') }} + +### Error Response {: #error-response } + +{{ schema_fields('types/error_response', 'location/rest') }} + +## Conformance + +A conforming REST transport implementation **MUST**: + +1. Implement endpoints for each location capability advertised in the business's UCP profile, + per their respective capability requirements ([Search](search.md), [Lookup](lookup.md)). + Each capability **MAY** be adopted independently. +2. Default to business-derived coordinates based on user location hint provided in `context.json` + for proximity (`distance`) filters when explicit coordinates are omitted by the platform in the request. +3. Support cursor-based pagination with a default limit of 10 for search results. +4. Return HTTP 200 for lookup requests; unknown identifiers result in fewer or no locations + returned (**MAY** include informational `not_found` messages). +5. Return HTTP 400 with `request_too_large` error for requests exceeding batch size limits. diff --git a/docs/specification/location/search.md b/docs/specification/location/search.md new file mode 100644 index 000000000..5e920e00e --- /dev/null +++ b/docs/specification/location/search.md @@ -0,0 +1,123 @@ + + +# Location Search Capability + +* **Capability Name:** `dev.ucp.common.location.search` + +Performs a search for physical locations (e.g., retail stores, restaurants, +warehouses). Supports natural language queries, geographic proximity (distance) +searches, and structured filtering by operating hours and offerings such as +amenities and inventory availability. + +## Operation + +| Operation | Description | +| :--- | :--- | +| **Search Locations** | Search for locations using query text, context, and filters. | + +### Request + +{{ extension_schema_fields('location_search.json#/$defs/search_request', 'location') }} + +### Response + +{{ extension_schema_fields('location_search.json#/$defs/search_response', 'location') }} + +## Search Inputs + +A valid search request **MUST** include at least one of: a `query` string +or one or more `filters`. When `query` is omitted, the request represents +a browse operation — the business returns locations matching the provided +filters without text-relevance ranking. + +Implementations **MUST** validate that incoming requests contain at least one +recognized input and **SHOULD** reject empty or invalid requests with an +appropriate error. Implementations define and enforce their own rules for +input presence and content — for example, requiring `query`, rejecting +empty `query` strings, or accepting filter-only requests. + +## Search Filters + +Location filters allow narrowing results based on specific criteria. +Standard filters are defined as below; businesses **MAY** support additional +custom filters via `additionalProperties`. + +{{ schema_fields('types/location_filter', 'location') }} + +### Hours-Based Filter + +Filters locations based on their operating hours: + +* `open_now`: A quick boolean filter to find locations currently open. +* `open_at`: An RFC 3339 date-time string to find locations open at a + specific future time (e.g., planning a visit or ordering ahead). + The business resolves this against the location's local time and timezone. + +### Offerings-Based Filter + +Separates static location characteristics from dynamic availability: + +* **`amenities`** (Array of Strings): Static features or services of the + location (e.g., `free_wifi`, `parking`, `outdoor_seating`, `curbside_pickup`). + All specified amenities **MUST** be supported by the location (AND semantic). +* **`inventory`** (Array of Objects): Real-time availability of items/goods at + the location. Some industry specific use cases include: + * *Shopping*: Checking stock levels for specific products or variants. + * *Food Ordering*: Checking availability of specific dishes or menu items. + Each inventory filter requires an `id` (product/dish ID) and can optionally specify + a `type` to help multi-industry business with backend routing and a + minimum `quantity`. + +### Geographic & Geofencing Filter + +Supports two distinct, industry-agnostic spatial search models: + +* **`distance` (Proximity Search)**: Filters for locations within a `max_distance` + (in RFC 7035 distance units = meters) of a `center` point. + +> **Privacy Integration**: If `center` is omitted, the server **MUST** use the +> user's address hint provided in the request `context` (which may be coarse/sanitized) +> to derive the center. + +* **`geofence_point` (Service Area Coverage)**: Filters for locations whose circular + service area (defined by `geofence_radius`) contains the specified point. + +## Pagination + +Cursor-based pagination for list operations. Cursors are opaque strings +that implementations MAY encode as stateless keyset tokens. + +### Page Size + +The `limit` parameter is a requested page size, not a guaranteed count. +Implementations **SHOULD** accept a page size of at least 10. When the +requested limit exceeds the implementation's maximum, implementations +**MAY** clamp to their maximum silently — returning fewer results without +error. Clients MUST NOT assume the response size equals the requested limit. + +### Pagination Request + +{{ extension_schema_fields('types/pagination.json#/$defs/request', 'location') }} + +### Pagination Response + +{{ extension_schema_fields('types/pagination.json#/$defs/response', 'location') }} + +## Transport Bindings + +* [REST Binding](rest.md#search): `POST /location/search` +* [MCP Binding](mcp.md#search_location): `search_location` tool diff --git a/mkdocs.yml b/mkdocs.yml index 8d77486d2..3abae00cc 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -74,6 +74,13 @@ nav: - REST: specification/order-rest.md - MCP: specification/order-mcp.md - Identity Linking Capability: specification/identity-linking.md + - Location Capability: + - Overview: specification/location/index.md + - Search: specification/location/search.md + - Lookup: specification/location/lookup.md + - Transports: + - REST: specification/location/rest.md + - MCP: specification/location/mcp.md - Payment Handlers: - Guide: specification/payment-handler-guide.md - Template: specification/payment-handler-template.md @@ -370,6 +377,24 @@ plugins: Model Context Protocol (MCP) transport binding for the Catalog Capability, mapping discovery operations to JSON-RPC tools with metadata validation rules. + Location Capability: + - specification/location/index.md: >- + Core Location Capability, detailing high-level product discovery + models, variant structures, merchant attribution, and real-time + pricing context. + - specification/location/search.md: >- + Product discovery via the Search Location capability, specifying + text queries and filters such as operating hours and offerings. + - specification/location/lookup.md: >- + Direct location retrieval via the Lookup Location capability, + specifying identifiers and additional filter option. + - specification/location/rest.md: >- + HTTP REST transport binding for the Location Capability, + detailing search and lookup endpoints with JSON payload examples. + - specification/location/mcp.md: >- + Model Context Protocol (MCP) transport binding for the Location + Capability, mapping discovery operations to JSON-RPC tools with + metadata validation rules. Other Capabilities: - specification/order.md: >- Post-purchase tracking via the Order Capability, detailing line diff --git a/source/schemas/common/location_lookup.json b/source/schemas/common/location_lookup.json new file mode 100644 index 000000000..14074882e --- /dev/null +++ b/source/schemas/common/location_lookup.json @@ -0,0 +1,59 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/location_lookup.json", + "name": "dev.ucp.common.location.lookup", + "title": "Location Lookup", + "description": "Location lookup by identifier. Supports batch retrieval (lookup_locations) and single-location detail (get_location).", + "type": "object", + "$defs": { + "lookup_request": { + "type": "object", + "description": "Request body for batch location lookup.", + "required": ["ids"], + "properties": { + "ids": { + "type": "array", + "items": { "type": "string" }, + "minItems": 1, + "description": "Identifiers of the locations to lookup. Implementations MUST support location ID." + }, + "filters": { + "$ref": "types/location_filter.json", + "description": "Filter criteria to narrow returned locations. All specified filters combine with AND logic." + }, + "context": { + "$ref": "types/context.json" + }, + "signals": { + "$ref": "types/signals.json" + } + } + }, + "lookup_response": { + "type": "object", + "required": [ + "ucp", + "locations" + ], + "properties": { + "ucp": { + "$ref": "../ucp.json#/$defs/response_catalog_schema" + }, + "locations": { + "type": "array", + "items": { + "$ref": "types/location.json" + }, + "description": "Locations matching the requested identifiers and filters. May contain fewer locations if some identifiers are not found." + }, + "messages": { + "type": "array", + "items": { + "$ref": "types/message.json" + }, + "description": "Errors, warnings, or informational messages about the requested locations." + } + } + } + } +} diff --git a/source/schemas/common/location_search.json b/source/schemas/common/location_search.json new file mode 100644 index 000000000..c2556e693 --- /dev/null +++ b/source/schemas/common/location_search.json @@ -0,0 +1,60 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/location_search.json", + "name": "dev.ucp.common.location.search", + "title": "Location Search", + "description": "Location search capability. Supports natural language queries, structured filtering, and pagination.", + "type": "object", + "$defs": { + "search_request": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Free-text search query for natural language location search (e.g., 'restaurants near me that deliver', 'hotels with pool')." + }, + "context": { + "$ref": "types/context.json" + }, + "signals": { + "$ref": "types/signals.json" + }, + "filters": { + "$ref": "types/location_filter.json" + }, + "pagination": { + "$ref": "types/pagination.json#/$defs/request" + } + } + }, + "search_response": { + "type": "object", + "required": [ + "ucp", + "locations" + ], + "properties": { + "ucp": { + "$ref": "../ucp.json#/$defs/response_catalog_schema" + }, + "locations": { + "type": "array", + "items": { + "$ref": "types/location.json" + }, + "description": "Locations matching the search criteria." + }, + "pagination": { + "$ref": "types/pagination.json#/$defs/response" + }, + "messages": { + "type": "array", + "items": { + "$ref": "types/message.json" + }, + "description": "Errors, warnings, or informational messages about the search results." + } + } + } + } +} diff --git a/source/schemas/shopping/types/context.json b/source/schemas/common/types/context.json similarity index 97% rename from source/schemas/shopping/types/context.json rename to source/schemas/common/types/context.json index 9d7f51207..e3443f4e2 100644 --- a/source/schemas/shopping/types/context.json +++ b/source/schemas/common/types/context.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://ucp.dev/schemas/shopping/types/context.json", + "$id": "https://ucp.dev/schemas/common/types/context.json", "title": "Context", "description": "Provisional buyer signals for relevance and localization—not authoritative data. Businesses SHOULD use these values when verified inputs (e.g., shipping address) are absent, and MAY ignore or down-rank them if inconsistent with higher-confidence signals (authenticated account, risk detection) or regulatory constraints (export controls). Eligibility and policy enforcement MUST occur at checkout time using binding transaction data. Context SHOULD be non-identifying and can be disclosed progressively—coarse signals early, finer resolution as the session progresses. Higher-resolution data (shipping address, billing address) supersedes context.", "type": "object", diff --git a/source/schemas/common/types/geo.json b/source/schemas/common/types/geo.json index 75d84fca5..0336eccc9 100644 --- a/source/schemas/common/types/geo.json +++ b/source/schemas/common/types/geo.json @@ -20,7 +20,7 @@ }, "geofence_radius": { "type": "number", - "description": "Geofence radius in meters. Used for proximity detection." + "description": "Geofence radius in RFC 7035 distance unit (meters). Used for proximity detection." } } } diff --git a/source/schemas/common/types/location_filter.json b/source/schemas/common/types/location_filter.json new file mode 100644 index 000000000..a3be7395c --- /dev/null +++ b/source/schemas/common/types/location_filter.json @@ -0,0 +1,58 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/types/location_filters.json", + "title": "Location Filters", + "description": "Filter criteria to narrow location search/lookup results. All specified filters combine with AND logic.", + "type": "object", + "properties": { + "geo": { + "type": "object", + "description": "Filter locations by geographic proximity or geofence coverage.", + "properties": { + "distance": { + "type": "object", + "description": "Filter locations within a maximum distance from a center point (proximity search).", + "required": ["max_distance"], + "properties": { + "center": { + "$ref": "geo.json", + "description": "The center coordinates. If omitted, the user's course location hints in the request context MUST be used to derive the center." + }, + "max_distance": { + "type": "number", + "minimum": 0, + "description": "Maximum distance in RFC 7035 distance unit (meters)." + } + }, + "additionalProperties": false + }, + "geofence_point": { + "$ref": "geo.json", + "description": "Filter locations whose geofence contains a specific point (e.g., delivery area check).", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "hours": { + "type": "object", + "description": "Filter by operating hours. If both values are specified, behavior is implementation-defined (usually open_at takes precedence or OR logic is enforced).", + "properties": { + "open_now": { + "type": "boolean", + "description": "Only return locations that are currently open." + }, + "open_at": { + "type": "string", + "format": "date-time", + "description": "Only return locations that are open at the specified date and time (RFC 3339) in the location's local time." + } + }, + "additionalProperties": false + }, + "offerings": { + "$ref": "location_offering_filter.json" + } + }, + "additionalProperties": true +} diff --git a/source/schemas/common/types/location_offering_filter.json b/source/schemas/common/types/location_offering_filter.json new file mode 100644 index 000000000..0945fdb00 --- /dev/null +++ b/source/schemas/common/types/location_offering_filter.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/types/location_offering_filter.json", + "title": "Location Offering Filter", + "description": "Filter criteria for location offerings, separating static amenities/services from dynamic inventory.", + "type": "object", + "$defs": { + "inventory_filter": { + "type": "object", + "title": "Inventory Filter", + "description": "Filter for a specific physical item (e.g., retail product or dish) and its required availability.", + "required": ["id"], + "properties": { + "id": { + "type": "string", + "description": "The unique identifier of the item (e.g., product or variant ID in shopping, or dish ID in food ordering)." + }, + "type": { + "type": "string", + "description": "The type of the item, helping backend routing. Well-known values: 'product' (shopping), 'dish' (food)." + }, + "quantity": { + "type": "integer", + "minimum": 1, + "description": "Minimum quantity required to be available at the location." + } + }, + "additionalProperties": true + } + }, + "properties": { + "amenities": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Filter by static services, amenities, or capabilities of the location (e.g., 'free_wifi', 'wheelchair_accessible', 'parking', 'curbside_pickup'). Matches locations that have ALL the listed amenities (AND logic)." + }, + "inventory": { + "type": "array", + "items": { + "$ref": "#/$defs/inventory_filter" + }, + "description": "Filter by real-time availability of inventory (e.g., retail products or restaurant dishes) at the location. Matches locations that have ALL the listed items in stock (AND logic)." + } + }, + "additionalProperties": true +} diff --git a/source/schemas/shopping/types/signals.json b/source/schemas/common/types/signals.json similarity index 100% rename from source/schemas/shopping/types/signals.json rename to source/schemas/common/types/signals.json diff --git a/source/schemas/shopping/cart.json b/source/schemas/shopping/cart.json index e7c451727..ad80d7de3 100644 --- a/source/schemas/shopping/cart.json +++ b/source/schemas/shopping/cart.json @@ -62,7 +62,7 @@ } }, "context": { - "$ref": "types/context.json", + "$ref": "../common/types/context.json", "description": "Buyer signals for localization (country, region, postal_code). Merchant uses for pricing, availability, currency. Falls back to geo-IP if omitted.", "ucp_request": { "create": "optional", @@ -70,7 +70,7 @@ } }, "signals": { - "$ref": "types/signals.json", + "$ref": "../common/types/signals.json", "ucp_request": { "create": "optional", "update": "optional" diff --git a/source/schemas/shopping/catalog_lookup.json b/source/schemas/shopping/catalog_lookup.json index 2fe7e657a..08ee0b909 100644 --- a/source/schemas/shopping/catalog_lookup.json +++ b/source/schemas/shopping/catalog_lookup.json @@ -39,10 +39,10 @@ "description": "Filter criteria to narrow returned products and variants. All specified filters combine with AND logic." }, "context": { - "$ref": "types/context.json" + "$ref": "../common/types/context.json" }, "signals": { - "$ref": "types/signals.json" + "$ref": "../common/types/signals.json" }, "attribution": { "$ref": "types/attribution.json" @@ -112,10 +112,10 @@ "description": "Filter criteria to narrow returned variants. All specified filters combine with AND logic." }, "context": { - "$ref": "types/context.json" + "$ref": "../common/types/context.json" }, "signals": { - "$ref": "types/signals.json" + "$ref": "../common/types/signals.json" }, "attribution": { "$ref": "types/attribution.json" diff --git a/source/schemas/shopping/catalog_search.json b/source/schemas/shopping/catalog_search.json index 46bdb2670..ee9753539 100644 --- a/source/schemas/shopping/catalog_search.json +++ b/source/schemas/shopping/catalog_search.json @@ -14,10 +14,10 @@ "description": "Free-text search query." }, "context": { - "$ref": "types/context.json" + "$ref": "../common/types/context.json" }, "signals": { - "$ref": "types/signals.json" + "$ref": "../common/types/signals.json" }, "attribution": { "$ref": "types/attribution.json" diff --git a/source/schemas/shopping/checkout.json b/source/schemas/shopping/checkout.json index 7b5aa69d1..7ae7fe443 100644 --- a/source/schemas/shopping/checkout.json +++ b/source/schemas/shopping/checkout.json @@ -47,7 +47,7 @@ } }, "context": { - "$ref": "types/context.json", + "$ref": "../common/types/context.json", "ucp_request": { "create": "optional", "update": "optional", @@ -55,7 +55,7 @@ } }, "signals": { - "$ref": "types/signals.json", + "$ref": "../common/types/signals.json", "ucp_request": "optional" }, "attribution": { diff --git a/source/schemas/shopping/types/fulfillment_destination.json b/source/schemas/shopping/types/fulfillment_destination.json index 8ae54579c..b35b4aedb 100644 --- a/source/schemas/shopping/types/fulfillment_destination.json +++ b/source/schemas/shopping/types/fulfillment_destination.json @@ -10,7 +10,8 @@ "$ref": "shipping_destination.json" }, { - "$ref": "../../common/types/location.json" + "$ref": "../../common/types/location.json", + "description": "A pickup location (e.g. retail store, locker, etc.)." } ] } diff --git a/source/services/common/mcp.openrpc.json b/source/services/common/mcp.openrpc.json new file mode 100644 index 000000000..4efbde845 --- /dev/null +++ b/source/services/common/mcp.openrpc.json @@ -0,0 +1,97 @@ +{ + "openrpc": "1.3.2", + "info": { + "title": "UCP Common Service", + "description": "Canonical MCP/JSON-RPC interface for UCP Common services, including Location discovery. Schema references are logical pointers - actual payload shape is determined by negotiated capabilities.\n\n**Endpoint Resolution:** This spec defines methods only. The endpoint URL MUST be obtained from the merchant's discovery profile at `/.well-known/ucp` under `services[\"dev.ucp.common\"][transport=mcp].endpoint`. The server entry below is a placeholder for tooling compatibility." + }, + "servers": [ + { + "name": "business", + "url": "{endpoint}", + "description": "Business-provided endpoint from UCP discovery profile", + "variables": { + "endpoint": { + "default": "https://business.example.com/ucp/mcp", + "description": "Obtain from /.well-known/ucp → services[\"dev.ucp.common\"][transport=mcp].endpoint" + } + } + } + ], + "components": { + "schemas": { + "meta": { + "type": "object", + "description": "Request metadata mapping to UCP standard headers.", + "required": ["ucp-agent"], + "additionalProperties": true, + "properties": { + "ucp-agent": { + "type": "object", + "description": "Platform agent identification. Maps to HTTP UCP-Agent header.", + "required": ["profile"], + "properties": { + "profile": { + "type": "string", + "format": "uri", + "description": "URL to the platform's UCP profile document." + } + } + }, + "idempotency-key": { + "type": "string", + "format": "uuid", + "description": "Unique key for retry safety. Maps to HTTP Idempotency-Key header (optional for read-only operations)." + }, + "signature": { + "type": "string", + "description": "Detached JWS signature in format `..` for message integrity." + } + } + } + } + }, + "methods": [ + { + "name": "search_locations", + "summary": "Search for physical locations", + "description": "Search for physical locations (e.g., retail stores, restaurants) using query text, filters, and geo proximity.", + "params": [ + { + "name": "meta", + "required": true, + "schema": {"$ref": "#/components/schemas/meta"} + }, + { + "name": "location", + "required": true, + "schema": {"$ref": "../../schemas/common/location_search.json#/$defs/search_request"} + } + ], + "result": { + "name": "response", + "schema": {"$ref": "../../schemas/common/location_search.json#/$defs/search_response"} + } + }, + { + "name": "lookup_locations", + "summary": "Batch lookup locations by identifier", + "description": "Batch lookup of physical locations by their stable identifiers. Returns matching locations and warnings.", + "params": [ + { + "name": "meta", + "required": true, + "schema": {"$ref": "#/components/schemas/meta"} + }, + { + "name": "location", + "required": true, + "schema": {"$ref": "../../schemas/common/location_lookup.json#/$defs/lookup_request"} + } + ], + "result": { + "name": "response", + "schema": {"$ref": "../../schemas/common/location_lookup.json#/$defs/lookup_response"} + } + } + ] +} diff --git a/source/services/common/rest.openapi.json b/source/services/common/rest.openapi.json new file mode 100644 index 000000000..984eb2ac9 --- /dev/null +++ b/source/services/common/rest.openapi.json @@ -0,0 +1,259 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "UCP Common Service", + "description": "Canonical REST interface for UCP Common services, including Location discovery. Schema references are logical pointers - actual payload shape is determined by negotiated capabilities.\n\n**Endpoint Resolution:** This spec defines operations only. The base URL MUST be obtained from the merchant's discovery profile at `/.well-known/ucp` under `services[\"dev.ucp.common\"][transport=rest].endpoint`. The `{endpoint}` server variable below is a placeholder for tooling compatibility." + }, + "servers": [ + { + "url": "{endpoint}", + "description": "Business-provided endpoint from UCP discovery profile", + "variables": { + "endpoint": { + "default": "https://business.example.com/ucp", + "description": "Obtain from /.well-known/ucp → services[\"dev.ucp.common\"][transport=rest].endpoint" + } + } + } + ], + "paths": { + "/locations/search": { + "post": { + "operationId": "search_locations", + "summary": "Search Locations", + "description": "Search for physical locations (e.g., retail stores, restaurants) using query text, geo proximity, and structured filters (e.g., hours, amenities, inventory).", + "parameters": [ + { "$ref": "#/components/parameters/authorization" }, + { "$ref": "#/components/parameters/x_api_key" }, + { "$ref": "#/components/parameters/signature" }, + { "$ref": "#/components/parameters/signature_input" }, + { "$ref": "#/components/parameters/content_digest" }, + { "$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/location_search_request" } + } + } + }, + "responses": { + "200": { + "description": "Search results", + "headers": { + "Signature": { "$ref": "#/components/headers/signature" }, + "Signature-Input": { "$ref": "#/components/headers/signature_input" }, + "Content-Digest": { "$ref": "#/components/headers/content_digest" } + }, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/location_search_response" } + } + } + } + } + } + }, + "/locations/lookup": { + "post": { + "operationId": "lookup_locations", + "summary": "Batch Lookup Locations", + "description": "Lookup one or more physical locations by their stable identifiers.", + "parameters": [ + { "$ref": "#/components/parameters/authorization" }, + { "$ref": "#/components/parameters/x_api_key" }, + { "$ref": "#/components/parameters/signature" }, + { "$ref": "#/components/parameters/signature_input" }, + { "$ref": "#/components/parameters/content_digest" }, + { "$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/location_lookup_request" } + } + } + }, + "responses": { + "200": { + "description": "Lookup results", + "headers": { + "Signature": { "$ref": "#/components/headers/signature" }, + "Signature-Input": { "$ref": "#/components/headers/signature_input" }, + "Content-Digest": { "$ref": "#/components/headers/content_digest" } + }, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/location_lookup_response" } + } + } + } + } + } + } + }, + "components": { + "parameters": { + "authorization": { + "name": "Authorization", + "in": "header", + "required": false, + "schema": { + "type": "string" + }, + "description": "Contains OAuth token representing platform or user credentials." + }, + "x_api_key": { + "name": "X-API-Key", + "in": "header", + "required": false, + "schema": { + "type": "string" + }, + "description": "Reusable API key allocated to the platform by the business." + }, + "signature": { + "name": "Signature", + "in": "header", + "required": false, + "schema": { + "type": "string" + }, + "description": "RFC 9421 HTTP Message Signature. Format: `sig1=::`." + }, + "signature_input": { + "name": "Signature-Input", + "in": "header", + "required": false, + "schema": { + "type": "string" + }, + "description": "RFC 9421 Signature-Input header. Format: `sig1=(\"@method\" \"@path\" ...);created=;keyid=\"\"`." + }, + "content_digest": { + "name": "Content-Digest", + "in": "header", + "required": false, + "schema": { + "type": "string" + }, + "description": "Body digest per RFC 9530. Format: `sha-256=::`." + }, + "request_id": { + "name": "Request-Id", + "in": "header", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Unique UUID for tracing requests across components." + }, + "user_agent": { + "name": "User-Agent", + "in": "header", + "required": false, + "schema": { + "type": "string" + }, + "description": "Identifies the user agent string making the call." + }, + "ucp_agent": { + "name": "UCP-Agent", + "in": "header", + "required": true, + "schema": { + "type": "string" + }, + "description": "Identifies the UCP agent making the call, containing the signer's profile URI. Format: profile=\"https://example.com/.well-known/ucp\"." + }, + "content_type": { + "name": "Content-Type", + "in": "header", + "required": false, + "schema": { + "type": "string" + }, + "description": "Representation Metadata describing the body content." + }, + "accept": { + "name": "Accept", + "in": "header", + "required": false, + "schema": { + "type": "string" + }, + "description": "Content Negotiation, indicating accepted formats." + }, + "accept_language": { + "name": "Accept-Language", + "in": "header", + "required": false, + "schema": { + "type": "string" + }, + "description": "Preferred natural languages for localization." + }, + "accept_encoding": { + "name": "Accept-Encoding", + "in": "header", + "required": false, + "schema": { + "type": "string" + }, + "description": "Supported content-codings (compression)." + } + }, + "headers": { + "signature": { + "required": false, + "schema": { + "type": "string" + }, + "description": "RFC 9421 HTTP Message Signature for response." + }, + "signature_input": { + "required": false, + "schema": { + "type": "string" + }, + "description": "RFC 9421 Signature-Input header for response." + }, + "content_digest": { + "required": false, + "schema": { + "type": "string" + }, + "description": "Body digest per RFC 9530 for response." + } + }, + "schemas": { + "location_search_request": { + "$ref": "../../schemas/common/location_search.json#/$defs/search_request" + }, + "location_search_response": { + "$ref": "../../schemas/common/location_search.json#/$defs/search_response" + }, + "location_lookup_request": { + "$ref": "../../schemas/common/location_lookup.json#/$defs/lookup_request" + }, + "location_lookup_response": { + "$ref": "../../schemas/common/location_lookup.json#/$defs/lookup_response" + } + } + } +} From fe04eaaac2eda136a0c5eab23175ca1f4edce09e Mon Sep 17 00:00:00 2001 From: Jing Li Date: Thu, 25 Jun 2026 14:33:02 -0400 Subject: [PATCH 04/37] Minor wording updates in documentation and also fix rendering issues. --- docs/specification/cart-rest.md | 2 +- docs/specification/catalog/rest.md | 6 +++--- docs/specification/checkout-rest.md | 2 +- docs/specification/checkout.md | 10 +++++----- docs/specification/location/index.md | 7 ++++--- docs/specification/location/rest.md | 8 ++++---- docs/specification/location/search.md | 2 +- docs/specification/order-rest.md | 2 +- docs/specification/reference.md | 2 ++ main.py | 2 +- 10 files changed, 23 insertions(+), 20 deletions(-) diff --git a/docs/specification/cart-rest.md b/docs/specification/cart-rest.md index c6c700a2e..c68abc95d 100644 --- a/docs/specification/cart-rest.md +++ b/docs/specification/cart-rest.md @@ -472,7 +472,7 @@ All REST endpoints **MUST** be served over HTTPS with minimum TLS version 1.3. The following headers are defined for the HTTP binding and apply to all operations unless otherwise noted. -{{ header_fields('create_cart', 'rest.openapi.json') }} +{{ header_fields('create_cart', 'shopping/rest.openapi.json') }} ### Specific Header Requirements diff --git a/docs/specification/catalog/rest.md b/docs/specification/catalog/rest.md index cd5b2ca31..4561d1c56 100644 --- a/docs/specification/catalog/rest.md +++ b/docs/specification/catalog/rest.md @@ -71,7 +71,7 @@ Businesses advertise REST transport availability through their UCP profile at Maps to the [Catalog Search](search.md) capability. -{{ method_fields('search_catalog', 'rest.openapi.json', 'catalog/rest') }} +{{ method_fields('search_catalog', 'shopping/rest.openapi.json', 'catalog/rest') }} #### Example @@ -201,7 +201,7 @@ for supported identifiers, resolution behavior, and client correlation requireme The request body contains an array of identifiers and optional context that applies to all lookups in the batch. -{{ method_fields('lookup_catalog', 'rest.openapi.json', 'catalog/rest') }} +{{ method_fields('lookup_catalog', 'shopping/rest.openapi.json', 'catalog/rest') }} #### Example: Batch Lookup with Context @@ -353,7 +353,7 @@ messages indicating which identifiers were not found. 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') }} +{{ method_fields('get_product', 'shopping/rest.openapi.json', 'catalog/rest') }} #### Example: With Option Selection diff --git a/docs/specification/checkout-rest.md b/docs/specification/checkout-rest.md index 136ef3ef4..8f65f1dc7 100644 --- a/docs/specification/checkout-rest.md +++ b/docs/specification/checkout-rest.md @@ -1280,7 +1280,7 @@ place to set these expectations via `messages`. The following headers are defined for the HTTP binding and apply to all operations unless otherwise noted. -{{ header_fields('create_checkout', 'rest.openapi.json') }} +{{ header_fields('create_checkout', 'shopping/rest.openapi.json') }} ### Specific Header Requirements diff --git a/docs/specification/checkout.md b/docs/specification/checkout.md index 6e0d5e15e..c6819e16c 100644 --- a/docs/specification/checkout.md +++ b/docs/specification/checkout.md @@ -630,7 +630,7 @@ should accept an additional `cart_id` field for cart-to-checkout conversion. See [Cart → Cart-to-Checkout Conversion](cart.md#cart-to-checkout-conversion) for the field contract. -{{ method_fields('create_checkout', 'rest.openapi.json', 'checkout') }} +{{ method_fields('create_checkout', 'shopping/rest.openapi.json', 'checkout') }} ### Get Checkout @@ -643,7 +643,7 @@ checkout. The platform will honor the TTL provided by the business via `expires_at` at the time of checkout session creation. -{{ method_fields('get_checkout', 'rest.openapi.json', 'checkout') }} +{{ method_fields('get_checkout', 'shopping/rest.openapi.json', 'checkout') }} ### Update Checkout @@ -652,7 +652,7 @@ The platform is **REQUIRED** to send the entire checkout resource containing any data updates to write-only data fields. The resource provided in the request will replace the existing checkout session state on the business side. -{{ method_fields('update_checkout', 'rest.openapi.json', 'checkout') }} +{{ method_fields('update_checkout', 'shopping/rest.openapi.json', 'checkout') }} ### Complete Checkout @@ -668,7 +668,7 @@ to construct the order representation (i.e. information like `line_items`, After this call, other details will be updated through subsequent events as the order, and its associated items, moves through the supply chain. -{{ method_fields('complete_checkout', 'rest.openapi.json', 'checkout') }} +{{ method_fields('complete_checkout', 'shopping/rest.openapi.json', 'checkout') }} ### Cancel Checkout @@ -678,7 +678,7 @@ already canceled or completed), then businesses **SHOULD** send back an error indicating the operation is not allowed. Any checkout session with a status that is not equal to `completed` or `canceled` **SHOULD** be cancelable. -{{ method_fields('cancel_checkout', 'rest.openapi.json', 'checkout') }} +{{ method_fields('cancel_checkout', 'shopping/rest.openapi.json', 'checkout') }} ## Transport Bindings diff --git a/docs/specification/location/index.md b/docs/specification/location/index.md index 75bb84cf7..ff1a4e50c 100644 --- a/docs/specification/location/index.md +++ b/docs/specification/location/index.md @@ -63,9 +63,10 @@ other capabilities (like Catalog, Cart, and Checkout in Shopping): to locate nearby stores that have a specific item available, bridging the gap between online catalog browsing and physical store visits. 3. **Provisional vs. Authoritative Boundaries**: - * *Discovery Phase (Provisional)*: Location responses based on operating hours, amenity support, - real-time product stock, property-level amenities represent the business's *current terms* at the - time of query. They are **provisional signals** and are not binding commitments. + * *Discovery Phase (Provisional)*: Location responses based on operating hours, real-time inventory + availability, and amenities offerings represent the business's *current terms* at the + time of query. They are **provisional signals** (despite most, like hours & amenities, remain stable + overtime) and are not binding commitments. * *Checkout Phase (Authoritative)*: Final transaction terms that depend on a location (e.g., pickup) **MUST** be negotiated and finalized authoritatively. Discovery signals **SHOULD NOT** be cached or reused across sessions without re-validation. diff --git a/docs/specification/location/rest.md b/docs/specification/location/rest.md index 34ba26927..122bb0ee2 100644 --- a/docs/specification/location/rest.md +++ b/docs/specification/location/rest.md @@ -69,7 +69,7 @@ Location capabilities through their UCP profile at `/.well-known/ucp`. Maps to the [Location Search](search.md) capability. -{{ method_fields('search_locations', 'rest.openapi.json', 'location/rest') }} +{{ method_fields('search_locations', 'common/rest.openapi.json', 'location/rest') }} #### Example: Search for Grocery Stores with Local Delivery Coverage (Geofencing) @@ -146,7 +146,7 @@ Maps to the [Location Search](search.md) capability. } ``` -#### Example: Search for Electronics Stores with iPhone in stock (Store Finder) +#### Example: Search for Electronics Stores with Phone In-stock (Store Finder) === "Request" @@ -169,7 +169,7 @@ Maps to the [Location Search](search.md) capability. "offerings": { "inventory": [ { - "id": "item_id_iphone_15_pro", + "id": "item_id_phone_15_pro", "type": "product", "quantity": 1 } @@ -230,7 +230,7 @@ Maps to the [Location Search](search.md) capability. Maps to the [Location Lookup](lookup.md) capability. -{{ method_fields('lookup_locations', 'rest.openapi.json', 'location/rest') }} +{{ method_fields('lookup_locations', 'common/rest.openapi.json', 'location/rest') }} #### Example: Simple Lookup diff --git a/docs/specification/location/search.md b/docs/specification/location/search.md index 5e920e00e..dba33559a 100644 --- a/docs/specification/location/search.md +++ b/docs/specification/location/search.md @@ -72,7 +72,7 @@ Filters locations based on their operating hours: Separates static location characteristics from dynamic availability: * **`amenities`** (Array of Strings): Static features or services of the - location (e.g., `free_wifi`, `parking`, `outdoor_seating`, `curbside_pickup`). + location (e.g., `free_wifi`, `parking`, `outdoor_seating`, `curbside_pickup`, `vegetarian`). All specified amenities **MUST** be supported by the location (AND semantic). * **`inventory`** (Array of Objects): Real-time availability of items/goods at the location. Some industry specific use cases include: diff --git a/docs/specification/order-rest.md b/docs/specification/order-rest.md index 73818e32d..f453eda35 100644 --- a/docs/specification/order-rest.md +++ b/docs/specification/order-rest.md @@ -227,7 +227,7 @@ Returns the current-state snapshot of an order. ## HTTP Headers -{{ header_fields('get_order', 'rest.openapi.json') }} +{{ header_fields('get_order', 'shopping/rest.openapi.json') }} ### Specific Header Requirements diff --git a/docs/specification/reference.md b/docs/specification/reference.md index d84d3da49..fa55047c1 100644 --- a/docs/specification/reference.md +++ b/docs/specification/reference.md @@ -21,6 +21,8 @@ within the UCP. ## Capability Schemas +{{ auto_generate_schema_reference('.', 'reference', include_extensions=False, base_dir='source/schemas/common') }} + {{ auto_generate_schema_reference('.', 'reference', include_extensions=False) }} ## Type Schemas diff --git a/main.py b/main.py index 1873a70ff..3881979a4 100644 --- a/main.py +++ b/main.py @@ -27,7 +27,7 @@ # --- CONFIGURATION --- # Base directories for schema resolution -OPENAPI_DIR = Path("source/services/shopping") +OPENAPI_DIR = Path("source/services") SCHEMAS_DIR = Path("source/schemas") HANDLERS_GOOGLE_PAY_DIR = Path("source/handlers/google_pay") COMMON_SCHEMAS_DIR = SCHEMAS_DIR / "common" From 111ea071ee0e220e1eceda00be4431b3ad9757af Mon Sep 17 00:00:00 2001 From: Jing Li Date: Thu, 25 Jun 2026 16:42:59 -0400 Subject: [PATCH 05/37] Minor fix on fulfillment retail location reference. --- docs/specification/fulfillment.md | 4 ++-- source/schemas/shopping/types/fulfillment_destination.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/specification/fulfillment.md b/docs/specification/fulfillment.md index ffb5658e8..2109f90c0 100644 --- a/docs/specification/fulfillment.md +++ b/docs/specification/fulfillment.md @@ -81,9 +81,9 @@ method. {{ schema_fields('types/shipping_destination_resp', 'fulfillment') }} -#### Retail Location +#### Location -{{ schema_fields('types/retail_location_resp', 'fulfillment') }} +{{ schema_fields('types/location_resp', 'fulfillment') }} #### Fulfillment Group diff --git a/source/schemas/shopping/types/fulfillment_destination.json b/source/schemas/shopping/types/fulfillment_destination.json index b35b4aedb..736bb6e93 100644 --- a/source/schemas/shopping/types/fulfillment_destination.json +++ b/source/schemas/shopping/types/fulfillment_destination.json @@ -11,7 +11,7 @@ }, { "$ref": "../../common/types/location.json", - "description": "A pickup location (e.g. retail store, locker, etc.)." + "description": "A pickup retail location (e.g. store, locker, etc.)." } ] } From a14bfeae3b66b446e5e0e4212837e9d6d7ea6240 Mon Sep 17 00:00:00 2001 From: Jing Li Date: Thu, 25 Jun 2026 19:16:55 -0400 Subject: [PATCH 06/37] Fix on rendering links. --- docs/specification/order.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/specification/order.md b/docs/specification/order.md index e28e79338..1e48c637b 100644 --- a/docs/specification/order.md +++ b/docs/specification/order.md @@ -433,7 +433,7 @@ See [Message Signatures](signatures.md) for more details. | `Webhook-Timestamp` | Event occurrence timestamp (unix) | | `Webhook-Id` | Unique event identifier | -{{ method_fields('order_event_webhook', 'rest.openapi.json', 'order') }} +{{ method_fields('order_event_webhook', 'shopping/rest.openapi.json', 'order') }} ### Webhook URL Configuration From 43f7df535f1be6744559dca5a26b74e23d7a1575 Mon Sep 17 00:00:00 2001 From: Jing Li Date: Thu, 25 Jun 2026 19:46:05 -0400 Subject: [PATCH 07/37] More fixes on rendering links. --- docs/specification/location/mcp.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/specification/location/mcp.md b/docs/specification/location/mcp.md index 5eb2e5d41..aa946940d 100644 --- a/docs/specification/location/mcp.md +++ b/docs/specification/location/mcp.md @@ -309,10 +309,6 @@ All application-level outcomes return a successful JSON-RPC result with the UCP {{ schema_fields('types/location_offering_filter', 'location/mcp') }} -### Location Get Result {: #location-get-result-schema } - -{{ schema_fields('services/common/mcp.openrpc.json#/components/schemas/location_get_result', 'location/mcp') }} - ### Error Response {: #error-response } {{ schema_fields('types/error_response', 'location/mcp') }} From 3269387509afd676cad83797fb993c1cb299fafa Mon Sep 17 00:00:00 2001 From: Jing Li Date: Mon, 29 Jun 2026 11:48:46 -0400 Subject: [PATCH 08/37] Fix location_filter file name. --- source/schemas/common/types/location_filter.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/schemas/common/types/location_filter.json b/source/schemas/common/types/location_filter.json index a3be7395c..358b0da17 100644 --- a/source/schemas/common/types/location_filter.json +++ b/source/schemas/common/types/location_filter.json @@ -1,7 +1,7 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://ucp.dev/schemas/common/types/location_filters.json", - "title": "Location Filters", + "$id": "https://ucp.dev/schemas/common/types/location_filter.json", + "title": "Location Filter", "description": "Filter criteria to narrow location search/lookup results. All specified filters combine with AND logic.", "type": "object", "properties": { From bb67299e7dcb0e50f3a0b4f3542020f052b706a5 Mon Sep 17 00:00:00 2001 From: Jing Li Date: Mon, 29 Jun 2026 13:25:50 -0400 Subject: [PATCH 09/37] Clean up remaining broken links and minor style updates. --- docs/specification/location/search.md | 6 ++--- source/schemas/common/types/daily_hour.json | 4 +-- .../schemas/common/types/exception_hour.json | 4 +-- source/schemas/common/types/geo.json | 2 +- .../common/types/inventory_filter.json | 24 +++++++++++++++++ .../types/location_offering_filter.json | 26 +------------------ 6 files changed, 33 insertions(+), 33 deletions(-) create mode 100644 source/schemas/common/types/inventory_filter.json diff --git a/docs/specification/location/search.md b/docs/specification/location/search.md index dba33559a..75286f45f 100644 --- a/docs/specification/location/search.md +++ b/docs/specification/location/search.md @@ -99,7 +99,7 @@ Supports two distinct, industry-agnostic spatial search models: ## Pagination Cursor-based pagination for list operations. Cursors are opaque strings -that implementations MAY encode as stateless keyset tokens. +that implementations **MAY** encode as stateless keyset tokens. ### Page Size @@ -119,5 +119,5 @@ error. Clients MUST NOT assume the response size equals the requested limit. ## Transport Bindings -* [REST Binding](rest.md#search): `POST /location/search` -* [MCP Binding](mcp.md#search_location): `search_location` tool +* [REST Binding](rest.md#post-locationssearch): `POST /location/search` +* [MCP Binding](mcp.md#search_locations): `search_locations` tool diff --git a/source/schemas/common/types/daily_hour.json b/source/schemas/common/types/daily_hour.json index 43f241728..b1ad8f77e 100644 --- a/source/schemas/common/types/daily_hour.json +++ b/source/schemas/common/types/daily_hour.json @@ -1,7 +1,7 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://ucp.dev/schemas/common/types/daily_hours.json", - "title": "Daily Hours", + "$id": "https://ucp.dev/schemas/common/types/daily_hour.json", + "title": "Daily Hour", "description": "Operating hours for a specific day of the week.", "type": "object", "required": ["day"], diff --git a/source/schemas/common/types/exception_hour.json b/source/schemas/common/types/exception_hour.json index 2bfb1cb38..66ba044cb 100644 --- a/source/schemas/common/types/exception_hour.json +++ b/source/schemas/common/types/exception_hour.json @@ -1,7 +1,7 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://ucp.dev/schemas/common/types/exception_hours.json", - "title": "Exception Hours", + "$id": "https://ucp.dev/schemas/common/types/exception_hour.json", + "title": "Exception Hour", "description": "Operating hours for a specific date (e.g., holiday or temporary change).", "type": "object", "required": ["date"], diff --git a/source/schemas/common/types/geo.json b/source/schemas/common/types/geo.json index 0336eccc9..bd9f4dccb 100644 --- a/source/schemas/common/types/geo.json +++ b/source/schemas/common/types/geo.json @@ -1,7 +1,7 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://ucp.dev/schemas/common/types/geo.json", - "title": "Geo Representation", + "title": "Geo", "description": "Geographic information.", "type": "object", "required": ["latitude", "longitude"], diff --git a/source/schemas/common/types/inventory_filter.json b/source/schemas/common/types/inventory_filter.json new file mode 100644 index 000000000..0abe9e651 --- /dev/null +++ b/source/schemas/common/types/inventory_filter.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/types/inventory_filter.json", + "title": "Inventory Filter", + "description": "Filter for a specific inventory and its required availability.", + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "string", + "description": "The unique identifier of the item (e.g., product or variant ID in shopping, or dish ID in food ordering)." + }, + "type": { + "type": "string", + "description": "The type of the item, helping backend routing. Well-known values: 'product' (shopping), 'dish' (food)." + }, + "quantity": { + "type": "integer", + "minimum": 1, + "description": "Minimum quantity required to be available at the location." + } + }, + "additionalProperties": true +} diff --git a/source/schemas/common/types/location_offering_filter.json b/source/schemas/common/types/location_offering_filter.json index 0945fdb00..05c62ec00 100644 --- a/source/schemas/common/types/location_offering_filter.json +++ b/source/schemas/common/types/location_offering_filter.json @@ -4,30 +4,6 @@ "title": "Location Offering Filter", "description": "Filter criteria for location offerings, separating static amenities/services from dynamic inventory.", "type": "object", - "$defs": { - "inventory_filter": { - "type": "object", - "title": "Inventory Filter", - "description": "Filter for a specific physical item (e.g., retail product or dish) and its required availability.", - "required": ["id"], - "properties": { - "id": { - "type": "string", - "description": "The unique identifier of the item (e.g., product or variant ID in shopping, or dish ID in food ordering)." - }, - "type": { - "type": "string", - "description": "The type of the item, helping backend routing. Well-known values: 'product' (shopping), 'dish' (food)." - }, - "quantity": { - "type": "integer", - "minimum": 1, - "description": "Minimum quantity required to be available at the location." - } - }, - "additionalProperties": true - } - }, "properties": { "amenities": { "type": "array", @@ -39,7 +15,7 @@ "inventory": { "type": "array", "items": { - "$ref": "#/$defs/inventory_filter" + "$ref": "inventory_filter.json" }, "description": "Filter by real-time availability of inventory (e.g., retail products or restaurant dishes) at the location. Matches locations that have ALL the listed items in stock (AND logic)." } From 8a3ec67393009a7d7676ddbf27dcccee8cc1376e Mon Sep 17 00:00:00 2001 From: Jing Li Date: Wed, 15 Jul 2026 14:48:23 -0400 Subject: [PATCH 10/37] Address comments that involve minor schema changes. --- source/schemas/common/types/inventory_filter.json | 6 +----- source/schemas/common/types/location_filter.json | 2 +- source/schemas/common/types/signals.json | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/source/schemas/common/types/inventory_filter.json b/source/schemas/common/types/inventory_filter.json index 0abe9e651..437f535cb 100644 --- a/source/schemas/common/types/inventory_filter.json +++ b/source/schemas/common/types/inventory_filter.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://ucp.dev/schemas/common/types/inventory_filter.json", "title": "Inventory Filter", - "description": "Filter for a specific inventory and its required availability.", + "description": "Filter for a specific inventory and its required availability. `id` is dependent on the industry offering the inventory (e.g., product/variant ID from shopping catalog, dish ID from food menu).", "type": "object", "required": ["id"], "properties": { @@ -10,10 +10,6 @@ "type": "string", "description": "The unique identifier of the item (e.g., product or variant ID in shopping, or dish ID in food ordering)." }, - "type": { - "type": "string", - "description": "The type of the item, helping backend routing. Well-known values: 'product' (shopping), 'dish' (food)." - }, "quantity": { "type": "integer", "minimum": 1, diff --git a/source/schemas/common/types/location_filter.json b/source/schemas/common/types/location_filter.json index 358b0da17..5f8520a19 100644 --- a/source/schemas/common/types/location_filter.json +++ b/source/schemas/common/types/location_filter.json @@ -16,7 +16,7 @@ "properties": { "center": { "$ref": "geo.json", - "description": "The center coordinates. If omitted, the user's course location hints in the request context MUST be used to derive the center." + "description": "The center coordinates. If omitted, the user's coarse location hints in the request context MUST be used to derive the center." }, "max_distance": { "type": "number", diff --git a/source/schemas/common/types/signals.json b/source/schemas/common/types/signals.json index dfd58d0de..85421d863 100644 --- a/source/schemas/common/types/signals.json +++ b/source/schemas/common/types/signals.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://ucp.dev/schemas/shopping/types/signals.json", + "$id": "https://ucp.dev/schemas/common/types/signals.json", "title": "Signals", "description": "Environment data provided by the platform to support authorization and abuse prevention. Values MUST NOT be buyer-asserted claims — platforms provide signals based on direct observation or independently verifiable third-party attestations. All signal keys MUST use reverse-domain naming to ensure provenance and prevent collisions when multiple extensions contribute to the shared namespace.", "type": "object", From c12499f29304f717eff305b550e08cf47b46773f Mon Sep 17 00:00:00 2001 From: Jing Li Date: Wed, 15 Jul 2026 18:30:07 -0400 Subject: [PATCH 11/37] Address rest of the comments from feedback. --- docs/specification/location/rest.md | 1 - source/schemas/common/types/daily_hour.json | 2 +- source/schemas/common/types/exception_hour.json | 2 +- source/schemas/common/types/location.json | 6 +++--- source/schemas/common/types/location_filter.json | 10 ++++------ source/schemas/common/types/time_interval.json | 4 ++-- 6 files changed, 11 insertions(+), 14 deletions(-) diff --git a/docs/specification/location/rest.md b/docs/specification/location/rest.md index 122bb0ee2..9797cd445 100644 --- a/docs/specification/location/rest.md +++ b/docs/specification/location/rest.md @@ -170,7 +170,6 @@ Maps to the [Location Search](search.md) capability. "inventory": [ { "id": "item_id_phone_15_pro", - "type": "product", "quantity": 1 } ] diff --git a/source/schemas/common/types/daily_hour.json b/source/schemas/common/types/daily_hour.json index b1ad8f77e..e059d7f6a 100644 --- a/source/schemas/common/types/daily_hour.json +++ b/source/schemas/common/types/daily_hour.json @@ -12,7 +12,7 @@ }, "is_closed": { "type": "boolean", - "description": "If true, the location is closed on this day. When true, intervals MUST be omitted." + "description": "If true, the location is closed on this day. When true, is_24_hours and intervals MUST be omitted." }, "is_24_hours": { "type": "boolean", diff --git a/source/schemas/common/types/exception_hour.json b/source/schemas/common/types/exception_hour.json index 66ba044cb..57e9880e9 100644 --- a/source/schemas/common/types/exception_hour.json +++ b/source/schemas/common/types/exception_hour.json @@ -17,7 +17,7 @@ }, "is_closed": { "type": "boolean", - "description": "If true, the location is closed on this date. When true, intervals MUST be omitted." + "description": "If true, the location is closed on this date. When true, is_24_hours and intervals MUST be omitted." }, "is_24_hours": { "type": "boolean", diff --git a/source/schemas/common/types/location.json b/source/schemas/common/types/location.json index f29fe2e49..24678e3f2 100644 --- a/source/schemas/common/types/location.json +++ b/source/schemas/common/types/location.json @@ -40,7 +40,7 @@ }, "hours": { "type": "array", - "description": "Regular weekly operating hours. For overnight hours, use two entries across two days.", + "description": "Regular weekly operating hours. For overnight hours, use two entries across two days (first entry ending with 24:00 and second entry starting 00:00).", "items": { "$ref": "daily_hour.json" }, @@ -48,7 +48,7 @@ }, "exception_hours": { "type": "array", - "description": "Exception hours for specific dates (holidays, closures, etc.).", + "description": "Exception hours for specific dates (holidays, closures, etc.). For overnight hours, use two entries across two days (first entry ending with 24:00 and second entry starting 00:00).", "items": { "$ref": "exception_hour.json" }, @@ -56,7 +56,7 @@ }, "timezone": { "type": "string", - "description": "IANA timezone identifier (e.g., 'America/New_York'). MUST be set when hours or exception_hours are present Required for correct interpretation.", + "description": "IANA timezone identifier (e.g., 'America/New_York'). MUST be set when hours or exception_hours are present. Required for correct interpretation.", "ucp_request": "omit" } } diff --git a/source/schemas/common/types/location_filter.json b/source/schemas/common/types/location_filter.json index 5f8520a19..7055b4324 100644 --- a/source/schemas/common/types/location_filter.json +++ b/source/schemas/common/types/location_filter.json @@ -16,7 +16,7 @@ "properties": { "center": { "$ref": "geo.json", - "description": "The center coordinates. If omitted, the user's coarse location hints in the request context MUST be used to derive the center." + "description": "The center coordinates. If omitted, the user's coarse location hints in the request context MUST be used to derive the center via forward geocoding strategies." }, "max_distance": { "type": "number", @@ -28,8 +28,7 @@ }, "geofence_point": { "$ref": "geo.json", - "description": "Filter locations whose geofence contains a specific point (e.g., delivery area check).", - "additionalProperties": false + "description": "Filter locations whose geofence contains a specific point (e.g., delivery area check)." } }, "additionalProperties": false @@ -45,7 +44,7 @@ "open_at": { "type": "string", "format": "date-time", - "description": "Only return locations that are open at the specified date and time (RFC 3339) in the location's local time." + "description": "An RFC 3339 instant. The business converts it to the location's local timezone and evaluates against the known hours." } }, "additionalProperties": false @@ -53,6 +52,5 @@ "offerings": { "$ref": "location_offering_filter.json" } - }, - "additionalProperties": true + } } diff --git a/source/schemas/common/types/time_interval.json b/source/schemas/common/types/time_interval.json index c9d2d568b..f9eedd129 100644 --- a/source/schemas/common/types/time_interval.json +++ b/source/schemas/common/types/time_interval.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://ucp.dev/schemas/common/types/time_interval.json", "title": "Time Interval", - "description": "An open time interval with 24-hour HH:MM format.", + "description": "An open time interval with 24-hour HH:MM format. `close` MUST be later than `open`", "type": "object", "required": ["open", "close"], "properties": { @@ -13,7 +13,7 @@ }, "close": { "type": "string", - "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$", + "pattern": "^(([01][0-9]|2[0-3]):[0-5][0-9]|24:00)$", "description": "End time (e.g., '18:00')." } } From 80cb4ee77511935e1f15b4d910ce80d1912ff54d Mon Sep 17 00:00:00 2001 From: Jing Li Date: Wed, 15 Jul 2026 19:27:22 -0400 Subject: [PATCH 12/37] Fix broken doc build link. --- docs/specification/catalog/rest.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/specification/catalog/rest.md b/docs/specification/catalog/rest.md index 4561d1c56..866c91803 100644 --- a/docs/specification/catalog/rest.md +++ b/docs/specification/catalog/rest.md @@ -514,7 +514,7 @@ with the same identifier. The following headers are defined for the HTTP binding and apply to all operations unless otherwise noted. -{{ header_fields('search_catalog', 'rest.openapi.json') }} +{{ header_fields('search_catalog', 'shopping/rest.openapi.json') }} ### Specific Header Requirements From 3c8a694c01d8ab0e0752b9d83d50c073e08f257e Mon Sep 17 00:00:00 2001 From: Ilya Grigorik Date: Wed, 15 Jul 2026 23:11:52 -0700 Subject: [PATCH 13/37] fix(location): mark custom filters as extensible Location search intentionally permits Business-defined filters, but the schema relied on JSON Schema's implicit open-object default while the prose named additionalProperties as the extension mechanism. Declare the extension point explicitly so schema readers and generated documentation can distinguish intentional extensibility from omission. Strict resolution remains a caller-selected closed-world override. --- source/schemas/common/types/location_filter.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/source/schemas/common/types/location_filter.json b/source/schemas/common/types/location_filter.json index 7055b4324..1d5d2d88c 100644 --- a/source/schemas/common/types/location_filter.json +++ b/source/schemas/common/types/location_filter.json @@ -52,5 +52,6 @@ "offerings": { "$ref": "location_offering_filter.json" } - } + }, + "additionalProperties": true } From 310b6458c34ffa2566646942639cadb3f43ab778 Mon Sep 17 00:00:00 2001 From: Ilya Grigorik Date: Wed, 15 Jul 2026 23:28:34 -0700 Subject: [PATCH 14/37] docs(location): remove catalog copy-paste Location documentation inherited Catalog-specific descriptions, rendering contexts, and a severity policy that Location never defined. It also documented a singular REST path and a filter name that do not exist in the binding/schema. Use Location-specific llms.txt descriptions and render scopes, remove the unsupported severity claim, and align the visible endpoint and filter names with their canonical definitions. --- docs/specification/location/index.md | 11 ++++------- docs/specification/location/search.md | 2 +- mkdocs.yml | 12 ++++++------ 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/docs/specification/location/index.md b/docs/specification/location/index.md index ff1a4e50c..fcb684771 100644 --- a/docs/specification/location/index.md +++ b/docs/specification/location/index.md @@ -46,7 +46,7 @@ This is vertical-agnostic and enables key commerce flows such as: * **Inventory**: Dynamic availability of goods (e.g., retail products or restaurant dishes). * **Geofencing**: Locations can define a `geofence_radius` around their coordinates. This is used to determine if a location can serve a specific user (e.g., delivery area check). - Clients can perform proximity searches (`distance` filter) or coverage checks (`geofence` filter) using the filter. + Clients can perform proximity searches (`distance` filter) or coverage checks (`geofence_point` filter) using the filter. * **Operating Hours**: Weekly schedules (`hours`) and date-specific overrides (`exception_hours` - e.g., holidays, temporary closures) associated with a timezone. @@ -109,20 +109,17 @@ Messages communicate business outcomes and provide context: | `warning` | Important conditions affecting purchase | `permanently_closed`, `temporary_closure` | | `info` | Additional context without issues | `not_found`, `holiday_hours_active` | -**Note**: Most catalog errors use `severity: "recoverable"` - agents -handle them programmatically (retry, inform user, show alternatives). - #### Message (Error) -{{ schema_fields('types/message_error', 'catalog') }} +{{ schema_fields('types/message_error', 'location') }} #### Message (Warning) -{{ schema_fields('types/message_warning', 'catalog') }} +{{ schema_fields('types/message_warning', 'location') }} #### Message (Info) -{{ schema_fields('types/message_info', 'catalog') }} +{{ schema_fields('types/message_info', 'location') }} ### Common Scenarios diff --git a/docs/specification/location/search.md b/docs/specification/location/search.md index 75286f45f..eee00a66d 100644 --- a/docs/specification/location/search.md +++ b/docs/specification/location/search.md @@ -119,5 +119,5 @@ error. Clients MUST NOT assume the response size equals the requested limit. ## Transport Bindings -* [REST Binding](rest.md#post-locationssearch): `POST /location/search` +* [REST Binding](rest.md#post-locationssearch): `POST /locations/search` * [MCP Binding](mcp.md#search_locations): `search_locations` tool diff --git a/mkdocs.yml b/mkdocs.yml index 3abae00cc..d47aa068c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -379,15 +379,15 @@ plugins: metadata validation rules. Location Capability: - specification/location/index.md: >- - Core Location Capability, detailing high-level product discovery - models, variant structures, merchant attribution, and real-time - pricing context. + Core Location Capability, detailing high-level physical location + discovery models, geographic and geofencing context, operating + hours, and offerings such as amenities and inventory. - specification/location/search.md: >- - Product discovery via the Search Location capability, specifying + Location discovery via the Location Search capability, specifying text queries and filters such as operating hours and offerings. - specification/location/lookup.md: >- - Direct location retrieval via the Lookup Location capability, - specifying identifiers and additional filter option. + Direct location retrieval via the Location Lookup capability, + specifying identifiers and optional filters. - specification/location/rest.md: >- HTTP REST transport binding for the Location Capability, detailing search and lookup endpoints with JSON payload examples. From 5e3ff05ab6cd83e4a645612fc6e3a4310cfca2d3 Mon Sep 17 00:00:00 2001 From: Jing Li Date: Wed, 29 Jul 2026 21:11:37 -0400 Subject: [PATCH 15/37] Add location into UCP glossary to resolve feedback on PR#642. --- docs/specification/glossary.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/specification/glossary.md b/docs/specification/glossary.md index 716e41f7f..6c9bcc8b3 100644 --- a/docs/specification/glossary.md +++ b/docs/specification/glossary.md @@ -40,12 +40,13 @@ acronym in each specification Markdown file spells out the full term (e.g., ## Commerce -| Term | Acronym | Definition | -| :--------------------------- | :------ | :---------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Business** | - | The entity selling goods or services. In UCP, they act as the **Merchant of Record (MoR)**, retaining financial liability and ownership of the order. | -| **Merchant of Record** | MoR | The legal entity responsible for the sale, including financial liability and order ownership. | -| **Payment Service Provider** | PSP | The financial infrastructure provider that processes payments, authorizations, and settlements on behalf of the business. | -| **Platform** | - | The consumer-facing surface (AI agent, app, website) acting on behalf of the user to discover businesses and facilitate commerce. | +| Term | Acronym | Definition | +| :--------------------------- | :------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Business** | - | The entity selling goods or services. In UCP, they act as the **Merchant of Record (MoR)**, retaining financial liability and ownership of the order. | +| **Merchant of Record** | MoR | The legal entity responsible for the sale, including financial liability and order ownership. | +| **Payment Service Provider** | PSP | The financial infrastructure provider that processes payments, authorizations, and settlements on behalf of the business. | +| **Platform** | - | The consumer-facing surface (AI agent, app, website) acting on behalf of the user to discover businesses and facilitate commerce. | +| **Location** | - | A physical entity that can be found on a map. Identified by a stable, business-scoped string (id) and referenced across UCP for user contextualization and location discovery.| ## Payments From 1bd596e7574f4ddb89a49d1ed33f03d81b75d89d Mon Sep 17 00:00:00 2001 From: Jing Li Date: Wed, 29 Jul 2026 21:56:40 -0400 Subject: [PATCH 16/37] Add security & privacy considerations. --- docs/specification/location/index.md | 14 ++++++++++++++ .../scaffolds/common_location_lookup_request.json | 3 +++ .../scaffolds/common_location_lookup_response.json | 6 ++++++ .../scaffolds/common_location_search_request.json | 3 +++ .../scaffolds/common_location_search_response.json | 6 ++++++ 5 files changed, 32 insertions(+) create mode 100644 scripts/scaffolds/common_location_lookup_request.json create mode 100644 scripts/scaffolds/common_location_lookup_response.json create mode 100644 scripts/scaffolds/common_location_search_request.json create mode 100644 scripts/scaffolds/common_location_search_response.json diff --git a/docs/specification/location/index.md b/docs/specification/location/index.md index fcb684771..65d56de8e 100644 --- a/docs/specification/location/index.md +++ b/docs/specification/location/index.md @@ -143,3 +143,17 @@ The capabilities above are bound to specific transport protocols: * [REST Binding](rest.md): RESTful API mapping. * [MCP Binding](mcp.md): Model Context Protocol mapping via JSON-RPC. + +## Security & Privacy Considerations + +1. **Coarse-by-default**: Platforms **SHOULD** default to sending coarse location hints (e.g., postal code or rounded coordinates) during the discovery phase. + Precise locations/coordinates **SHOULD** only be shared when the user explicitly consents or selects a specific location. +2. **Inventory Probing Mitigation**: Businesses **SHOULD** implement rate-limiting on search requests, especially if containing inventory availability filters, + to prevent scraping & aggressive numeration of the entire directory. +3. **Private/Dark Locations**: Businesses **MUST** filter out internal-only or non-user-accessible locations (e.g., dark kitchens, fulfillment-only hubs) + from search results. +4. **Physical Address Spoofing (Integrity)**: While location discovery is read-only, tampering with physical addresses in responses (e.g., through MITM attacks) + poses a physical safety/fraud risk. Platforms **SHOULD** verify signatures on location payloads before rendering them to users. +5. **Data Retention & Logging Sanitization**: Businesses **MUST NOT** persist precise location inputs beyond the lifecycle of the request, unless explicit user + consent is collected. Server logs should sanitize coordinate inputs by truncating decimal places (e.g., to 2 decimal places, ~1km accuracy) to prevent + accidental storage of precise user history. diff --git a/scripts/scaffolds/common_location_lookup_request.json b/scripts/scaffolds/common_location_lookup_request.json new file mode 100644 index 000000000..a2eeeab27 --- /dev/null +++ b/scripts/scaffolds/common_location_lookup_request.json @@ -0,0 +1,3 @@ +{ + "ids": ["scaffold_id"] +} diff --git a/scripts/scaffolds/common_location_lookup_response.json b/scripts/scaffolds/common_location_lookup_response.json new file mode 100644 index 000000000..57bc834bd --- /dev/null +++ b/scripts/scaffolds/common_location_lookup_response.json @@ -0,0 +1,6 @@ +{ + "ucp": { + "version": "2026-01-01" + }, + "locations": [] +} diff --git a/scripts/scaffolds/common_location_search_request.json b/scripts/scaffolds/common_location_search_request.json new file mode 100644 index 000000000..fe9fd770a --- /dev/null +++ b/scripts/scaffolds/common_location_search_request.json @@ -0,0 +1,3 @@ +{ + "query": "scaffold" +} diff --git a/scripts/scaffolds/common_location_search_response.json b/scripts/scaffolds/common_location_search_response.json new file mode 100644 index 000000000..57bc834bd --- /dev/null +++ b/scripts/scaffolds/common_location_search_response.json @@ -0,0 +1,6 @@ +{ + "ucp": { + "version": "2026-01-01" + }, + "locations": [] +} From e5a0cddea4c17e6bf1755c4175f54194b1649340 Mon Sep 17 00:00:00 2001 From: Jing Li Date: Thu, 30 Jul 2026 11:02:09 -0400 Subject: [PATCH 17/37] Address feedback on simplifying service area representation on location responses and finetune request filters. --- docs/specification/location/index.md | 8 +++---- docs/specification/location/lookup.md | 4 ++-- docs/specification/location/mcp.md | 14 +++++------ docs/specification/location/rest.md | 23 ++++++++----------- docs/specification/location/search.md | 10 ++++++-- source/schemas/common/types/geo.json | 4 ---- .../schemas/common/types/location_filter.json | 17 +++++++++++--- 7 files changed, 45 insertions(+), 35 deletions(-) diff --git a/docs/specification/location/index.md b/docs/specification/location/index.md index 65d56de8e..b04fb0ddd 100644 --- a/docs/specification/location/index.md +++ b/docs/specification/location/index.md @@ -37,16 +37,16 @@ This is vertical-agnostic and enables key commerce flows such as: ## Key Concepts * **Location**: A physical entity that can be found on a map. Defined by a display name, - address, operating hours, and **geographic context** (geographic coordinates and an - optional circular **geofence service radius** for delivery/service area checks). + address, operating hours, and **geographic context** (geographic coordinates). * **Offerings**: Features, capabilities, and inventory provided by the location. This is split into two distinct concepts to ensure tooling compatibility and semantic clarity: * **Amenities**: Static features, services, or capabilities of the location (e.g., `free_wifi`, `parking`, `outdoor_seating`, `curbside_pickup`). * **Inventory**: Dynamic availability of goods (e.g., retail products or restaurant dishes). -* **Geofencing**: Locations can define a `geofence_radius` around their coordinates. +* **Geofencing & Service Area**: Locations implicitly carry a geofence boundary around their coordinates. This is used to determine if a location can serve a specific user (e.g., delivery area check). - Clients can perform proximity searches (`distance` filter) or coverage checks (`geofence_point` filter) using the filter. + Clients can perform proximity searches (`distance` filter) or coverage checks (`geofence_point` filter) + using the filter. * **Operating Hours**: Weekly schedules (`hours`) and date-specific overrides (`exception_hours` - e.g., holidays, temporary closures) associated with a timezone. diff --git a/docs/specification/location/lookup.md b/docs/specification/location/lookup.md index 59e4a9deb..be1fbfc7c 100644 --- a/docs/specification/location/lookup.md +++ b/docs/specification/location/lookup.md @@ -56,10 +56,10 @@ to narrow down the returned locations. Filters use the same schema and AND semantics as [Search Filters](search.md#search-filters). Filters apply **after** identifier resolution. For example, if a client requests -`["loc_downtown", "loc_uptown"]` with a filter of `hours.open_now: true`: +`["loc_downtown", "loc_uptown"]` with a filter of `open_now: true`: 1. The server first resolves both identifiers to their respective locations. -2. The server then evaluates the `open_now` filter against each resolved location. +2. The server then evaluates the `open_now` hour filter against each resolved location. 3. If `loc_uptown` is currently closed, it is excluded, and only `loc_downtown` is returned. ### Request diff --git a/docs/specification/location/mcp.md b/docs/specification/location/mcp.md index aa946940d..eba13c5ac 100644 --- a/docs/specification/location/mcp.md +++ b/docs/specification/location/mcp.md @@ -144,9 +144,11 @@ Maps to the [Location Search](search.md) capability. "amenities": ["curbside_pickup"] }, "geo": { - "geofence_point": { - "latitude": 37.422, - "longitude": -122.084 + "serves": { + "point": { + "latitude": 37.422, + "longitude": -122.084 + } } } } @@ -186,8 +188,7 @@ Maps to the [Location Search](search.md) capability. }, "geo": { "latitude": 37.420, - "longitude": -122.080, - "geofence_radius": 10000 + "longitude": -122.080 }, "timezone": "America/Los_Angeles" } @@ -265,8 +266,7 @@ Maps to the [Location Lookup](lookup.md) capability. }, "geo": { "latitude": 40.707, - "longitude": -74.011, - "geofence_radius": 2000 + "longitude": -74.011 }, "timezone": "America/New_York" } diff --git a/docs/specification/location/rest.md b/docs/specification/location/rest.md index 9797cd445..74812e64b 100644 --- a/docs/specification/location/rest.md +++ b/docs/specification/location/rest.md @@ -98,9 +98,11 @@ Maps to the [Location Search](search.md) capability. "amenities": ["curbside_pickup"] }, "geo": { - "geofence_point": { - "latitude": 37.422, - "longitude": -122.084 + "serves": { + "point": { + "latitude": 37.422, + "longitude": -122.084 + } } } } @@ -137,8 +139,7 @@ Maps to the [Location Search](search.md) capability. }, "geo": { "latitude": 37.420, - "longitude": -122.080, - "geofence_radius": 10000 + "longitude": -122.080 }, "timezone": "America/Los_Angeles" } @@ -216,8 +217,7 @@ Maps to the [Location Search](search.md) capability. }, "geo": { "latitude": 40.709, - "longitude": -74.008, - "geofence_radius": 2000 + "longitude": -74.008 }, "timezone": "America/New_York" } @@ -277,8 +277,7 @@ Maps to the [Location Lookup](lookup.md) capability. }, "geo": { "latitude": 40.707, - "longitude": -74.011, - "geofence_radius": 2000 + "longitude": -74.011 }, "timezone": "America/New_York", "hours": [ @@ -331,8 +330,7 @@ Maps to the [Location Lookup](lookup.md) capability. }, "geo": { "latitude": 40.790, - "longitude": -73.950, - "geofence_radius": 1000 + "longitude": -73.950 }, "timezone": "America/New_York", "hours": [ @@ -423,8 +421,7 @@ Maps to the [Location Lookup](lookup.md) capability. }, "geo": { "latitude": 40.707, - "longitude": -74.011, - "geofence_radius": 2000 + "longitude": -74.011 }, "timezone": "America/New_York", "hours": [ diff --git a/docs/specification/location/search.md b/docs/specification/location/search.md index eee00a66d..6f01613f9 100644 --- a/docs/specification/location/search.md +++ b/docs/specification/location/search.md @@ -93,8 +93,14 @@ Supports two distinct, industry-agnostic spatial search models: > user's address hint provided in the request `context` (which may be coarse/sanitized) > to derive the center. -* **`geofence_point` (Service Area Coverage)**: Filters for locations whose circular - service area (defined by `geofence_radius`) contains the specified point. +* **`serves` (Service Area Coverage)**: Filters for locations that can serve + a target destination. The business evaluates coverage using their internal service area rules + (e.g., internal geometry, ZIP code lists) and returns qualifying locations only. + +> **Contextual Fallback**: If the `serves` filter is not explicitly specified in +> the request, the business **MAY** use the user's contextual location hints passed in the +> request `context` object to implicitly apply a `serves` filter, returning only locations +> that can service the user. ## Pagination diff --git a/source/schemas/common/types/geo.json b/source/schemas/common/types/geo.json index bd9f4dccb..1810480b3 100644 --- a/source/schemas/common/types/geo.json +++ b/source/schemas/common/types/geo.json @@ -17,10 +17,6 @@ "minimum": -180, "maximum": 180, "description": "Longitude in decimal degrees." - }, - "geofence_radius": { - "type": "number", - "description": "Geofence radius in RFC 7035 distance unit (meters). Used for proximity detection." } } } diff --git a/source/schemas/common/types/location_filter.json b/source/schemas/common/types/location_filter.json index 1d5d2d88c..b0d6ab9c3 100644 --- a/source/schemas/common/types/location_filter.json +++ b/source/schemas/common/types/location_filter.json @@ -26,9 +26,20 @@ }, "additionalProperties": false }, - "geofence_point": { - "$ref": "geo.json", - "description": "Filter locations whose geofence contains a specific point (e.g., delivery area check)." + "serves": { + "type": "object", + "description": "Filter locations that serve the target destination (e.g., delivery area geofence check).", + "properties": { + "point": { + "$ref": "geo.json", + "description": "Coordinates of the target destination." + }, + "address": { + "$ref": "locality.json", + "description": "Coarse postal address reference of the target destination (e.g., for ZIP-code based routing)." + } + }, + "additionalProperties": false } }, "additionalProperties": false From c4087e2de1c963cb7c42e27cafeb672f77de73e6 Mon Sep 17 00:00:00 2001 From: Jing Li Date: Thu, 30 Jul 2026 14:39:57 -0400 Subject: [PATCH 18/37] Update hours representation to be more consistent with standard schema representation. --- docs/specification/location/rest.md | 90 +++++++++++-------- source/schemas/common/types/daily_hour.json | 29 +++--- .../schemas/common/types/exception_hour.json | 48 +++++----- source/schemas/common/types/location.json | 4 +- .../schemas/common/types/time_interval.json | 4 +- 5 files changed, 87 insertions(+), 88 deletions(-) diff --git a/docs/specification/location/rest.md b/docs/specification/location/rest.md index 74812e64b..54617173f 100644 --- a/docs/specification/location/rest.md +++ b/docs/specification/location/rest.md @@ -283,38 +283,47 @@ Maps to the [Location Lookup](lookup.md) capability. "hours": [ { "day": "monday", - "intervals": [{"open": "09:00", "close": "21:00"}] + "open": "09:00", + "close": "21:00" }, { "day": "tuesday", - "intervals": [{"open": "09:00", "close": "21:00"}] + "open": "09:00", + "close": "12:00" + }, + { + "day": "tuesday", + "open": "13:00", + "close": "21:00" }, { "day": "wednesday", - "intervals": [{"open": "09:00", "close": "21:00"}] + "open": "09:00", + "close": "21:00" }, { "day": "thursday", - "intervals": [{"open": "09:00", "close": "21:00"}] + "open": "09:00", + "close": "21:00" }, { "day": "friday", - "intervals": [{"open": "09:00", "close": "22:00"}] + "open": "09:00", + "close": "22:00" }, { "day": "saturday", - "intervals": [{"open": "10:00", "close": "20:00"}] - }, - { - "day": "sunday", - "is_closed": true + "open": "10:00", + "close": "20:00" } ], "exception_hours": [ { - "date": "2026-11-26", + "from": "2026-11-26", + "through": "2026-11-26", "label": "Thanksgiving", - "is_closed": true + "open": "00:00", + "close": "00:00" } ] }, @@ -336,38 +345,37 @@ Maps to the [Location Lookup](lookup.md) capability. "hours": [ { "day": "monday", - "intervals": [{"open": "09:00", "close": "21:00"}] + "open": "09:00", + "close": "21:00" }, { "day": "tuesday", - "intervals": [{"open": "09:00", "close": "21:00"}] + "open": "09:00", + "close": "21:00" }, { "day": "wednesday", - "intervals": [{"open": "09:00", "close": "21:00"}] + "open": "09:00", + "close": "21:00" }, { "day": "thursday", - "intervals": [{"open": "09:00", "close": "21:00"}] + "open": "09:00", + "close": "21:00" }, { "day": "friday", - "intervals": [{"open": "09:00", "close": "22:00"}] - }, - { - "day": "saturday", - "is_closed": true - }, - { - "day": "sunday", - "is_closed": true + "open": "09:00", + "close": "22:00" } ], "exception_hours": [ { - "date": "2026-11-26", + "from": "2026-11-26", + "through": "2026-11-26", "label": "Thanksgiving", - "is_closed": true + "open": "00:00", + "close": "00:00" } ] } @@ -427,38 +435,42 @@ Maps to the [Location Lookup](lookup.md) capability. "hours": [ { "day": "monday", - "intervals": [{"open": "09:00", "close": "21:00"}] + "open": "09:00", + "close": "21:00" }, { "day": "tuesday", - "intervals": [{"open": "09:00", "close": "21:00"}] + "open": "09:00", + "close": "21:00" }, { "day": "wednesday", - "intervals": [{"open": "09:00", "close": "21:00"}] + "open": "09:00", + "close": "21:00" }, { "day": "thursday", - "intervals": [{"open": "09:00", "close": "21:00"}] + "open": "09:00", + "close": "21:00" }, { "day": "friday", - "intervals": [{"open": "09:00", "close": "22:00"}] + "open": "09:00", + "close": "22:00" }, { "day": "saturday", - "intervals": [{"open": "10:00", "close": "20:00"}] - }, - { - "day": "sunday", - "is_closed": true + "open": "10:00", + "close": "20:00" } ], "exception_hours": [ { - "date": "2026-11-26", + "from": "2026-11-26", + "through": "2026-11-26", "label": "Thanksgiving", - "is_closed": true + "open": "00:00", + "close": "00:00" } ] } diff --git a/source/schemas/common/types/daily_hour.json b/source/schemas/common/types/daily_hour.json index e059d7f6a..c95428413 100644 --- a/source/schemas/common/types/daily_hour.json +++ b/source/schemas/common/types/daily_hour.json @@ -5,25 +5,16 @@ "description": "Operating hours for a specific day of the week.", "type": "object", "required": ["day"], - "properties": { - "day": { - "type": "string", - "enum": ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"] - }, - "is_closed": { - "type": "boolean", - "description": "If true, the location is closed on this day. When true, is_24_hours and intervals MUST be omitted." - }, - "is_24_hours": { - "type": "boolean", - "description": "If true, open 24 hours on this day. When true, intervals MUST be omitted." - }, - "intervals": { - "type": "array", - "description": "One or more open intervals for this day. Supports split shifts.", - "items": { - "$ref": "time_interval.json" + "allOf": [ + { "$ref": "time_interval.json" }, + { + "type": "object", + "properties": { + "day": { + "type": "string", + "enum": ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"] + } } } - } + ] } diff --git a/source/schemas/common/types/exception_hour.json b/source/schemas/common/types/exception_hour.json index 57e9880e9..09b2dfdf9 100644 --- a/source/schemas/common/types/exception_hour.json +++ b/source/schemas/common/types/exception_hour.json @@ -2,33 +2,29 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://ucp.dev/schemas/common/types/exception_hour.json", "title": "Exception Hour", - "description": "Operating hours for a specific date (e.g., holiday or temporary change).", + "description": "Operating hours for a specific date range that differs from normal ones (e.g., holiday or temporary change). Omission of explicit intervals implies open 24 hours.", "type": "object", - "required": ["date"], - "properties": { - "date": { - "type": "string", - "format": "date", - "description": "An ISO 8601 date." - }, - "label": { - "type": "string", - "description": "Human readable explanation for the exception (e.g., 'Thanksgiving')." - }, - "is_closed": { - "type": "boolean", - "description": "If true, the location is closed on this date. When true, is_24_hours and intervals MUST be omitted." - }, - "is_24_hours": { - "type": "boolean", - "description": "If true, open 24 hours on this date. When true, intervals MUST be omitted." - }, - "intervals": { - "type": "array", - "description": "One or more open intervals for this date.", - "items": { - "$ref": "time_interval.json" + "required": ["from", "through"], + "allOf": [ + { "$ref": "time_interval.json" }, + { + "type": "object", + "properties": { + "from": { + "type": "string", + "format": "date", + "description": "An ISO 8601 date representing the start of the date range." + }, + "through": { + "type": "string", + "format": "date", + "description": "An ISO 8601 date representing the date after the end of the date range." + }, + "label": { + "type": "string", + "description": "Human readable explanation for the exception (e.g., 'Thanksgiving')." + } } } - } + ] } diff --git a/source/schemas/common/types/location.json b/source/schemas/common/types/location.json index 24678e3f2..49d07eeb6 100644 --- a/source/schemas/common/types/location.json +++ b/source/schemas/common/types/location.json @@ -40,7 +40,7 @@ }, "hours": { "type": "array", - "description": "Regular weekly operating hours. For overnight hours, use two entries across two days (first entry ending with 24:00 and second entry starting 00:00).", + "description": "Regular weekly operating hours. Support split hours through multiple entries on the same day and overnight hours (open > close). Omission of days implies full day closures.", "items": { "$ref": "daily_hour.json" }, @@ -48,7 +48,7 @@ }, "exception_hours": { "type": "array", - "description": "Exception hours for specific dates (holidays, closures, etc.). For overnight hours, use two entries across two days (first entry ending with 24:00 and second entry starting 00:00).", + "description": "Exception hours for specific dates (holidays, closures, etc.). Support split hours through multiple entries on the same day and overnight hours (open > close). For full day closures, MUST explicitly set open and close to 00:00.", "items": { "$ref": "exception_hour.json" }, diff --git a/source/schemas/common/types/time_interval.json b/source/schemas/common/types/time_interval.json index f9eedd129..c9d2d568b 100644 --- a/source/schemas/common/types/time_interval.json +++ b/source/schemas/common/types/time_interval.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://ucp.dev/schemas/common/types/time_interval.json", "title": "Time Interval", - "description": "An open time interval with 24-hour HH:MM format. `close` MUST be later than `open`", + "description": "An open time interval with 24-hour HH:MM format.", "type": "object", "required": ["open", "close"], "properties": { @@ -13,7 +13,7 @@ }, "close": { "type": "string", - "pattern": "^(([01][0-9]|2[0-3]):[0-5][0-9]|24:00)$", + "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$", "description": "End time (e.g., '18:00')." } } From e4d21896b5bd2227b02a85bcc3175269f1440275 Mon Sep 17 00:00:00 2001 From: Jing Li Date: Thu, 30 Jul 2026 15:28:14 -0400 Subject: [PATCH 19/37] Relax exact quantity based search and instead leverage availability status based coarse search during discovery phase. --- docs/specification/location/rest.md | 2 +- docs/specification/location/search.md | 9 ++++----- source/schemas/common/types/inventory_filter.json | 9 ++++----- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/docs/specification/location/rest.md b/docs/specification/location/rest.md index 54617173f..b59c77a9a 100644 --- a/docs/specification/location/rest.md +++ b/docs/specification/location/rest.md @@ -171,7 +171,7 @@ Maps to the [Location Search](search.md) capability. "inventory": [ { "id": "item_id_phone_15_pro", - "quantity": 1 + "availability_status": "in_stock" } ] }, diff --git a/docs/specification/location/search.md b/docs/specification/location/search.md index 6f01613f9..dd7cecc2e 100644 --- a/docs/specification/location/search.md +++ b/docs/specification/location/search.md @@ -76,11 +76,10 @@ Separates static location characteristics from dynamic availability: All specified amenities **MUST** be supported by the location (AND semantic). * **`inventory`** (Array of Objects): Real-time availability of items/goods at the location. Some industry specific use cases include: - * *Shopping*: Checking stock levels for specific products or variants. - * *Food Ordering*: Checking availability of specific dishes or menu items. - Each inventory filter requires an `id` (product/dish ID) and can optionally specify - a `type` to help multi-industry business with backend routing and a - minimum `quantity`. + * *Shopping*: Checking stock availability for specific products or variants. + * *Food Ordering*: Checking offering availability of specific dishes or menu items. + Each inventory filter requires an `id` (e.g., product/dish ID) and can optionally specify + a coarse `availability_status` value. ### Geographic & Geofencing Filter diff --git a/source/schemas/common/types/inventory_filter.json b/source/schemas/common/types/inventory_filter.json index 437f535cb..1166526dd 100644 --- a/source/schemas/common/types/inventory_filter.json +++ b/source/schemas/common/types/inventory_filter.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://ucp.dev/schemas/common/types/inventory_filter.json", "title": "Inventory Filter", - "description": "Filter for a specific inventory and its required availability. `id` is dependent on the industry offering the inventory (e.g., product/variant ID from shopping catalog, dish ID from food menu).", + "description": "Filter for a specific inventory based on its availability status. `id` is dependent on the industry offering the inventory (e.g., product/variant ID from shopping catalog, dish ID from food menu).", "type": "object", "required": ["id"], "properties": { @@ -10,10 +10,9 @@ "type": "string", "description": "The unique identifier of the item (e.g., product or variant ID in shopping, or dish ID in food ordering)." }, - "quantity": { - "type": "integer", - "minimum": 1, - "description": "Minimum quantity required to be available at the location." + "availability_status": { + "type": "string", + "description": "Availability status of the inventory requested at the location. Well-known values: `in_stock`, `backorder`, `preorder`." } }, "additionalProperties": true From 918c1cb61e6532930d6625a616088eb92b834935 Mon Sep 17 00:00:00 2001 From: Jing Li Date: Thu, 30 Jul 2026 16:42:31 -0400 Subject: [PATCH 20/37] Standardize amenities vocabulary and also restructure filtering model between it and dynamic inventory filter. --- docs/specification/location/mcp.md | 14 ++++----- docs/specification/location/rest.md | 31 ++++++++++--------- docs/specification/location/search.md | 9 ++++-- source/schemas/common/types/amenity_type.json | 12 +++++++ source/schemas/common/types/location.json | 8 +++++ .../schemas/common/types/location_filter.json | 15 +++++++-- .../types/location_offering_filter.json | 24 -------------- 7 files changed, 63 insertions(+), 50 deletions(-) create mode 100644 source/schemas/common/types/amenity_type.json delete mode 100644 source/schemas/common/types/location_offering_filter.json diff --git a/docs/specification/location/mcp.md b/docs/specification/location/mcp.md index eba13c5ac..4f0217810 100644 --- a/docs/specification/location/mcp.md +++ b/docs/specification/location/mcp.md @@ -140,9 +140,7 @@ Maps to the [Location Search](search.md) capability. "hours": { "open_now": true }, - "offerings": { - "amenities": ["curbside_pickup"] - }, + "amenities": ["curbside_pickup"], "geo": { "serves": { "point": { @@ -190,6 +188,7 @@ Maps to the [Location Search](search.md) capability. "latitude": 37.420, "longitude": -122.080 }, + "amenities": ["curbside_pickup", "in_store_pickup", "parking"], "timezone": "America/Los_Angeles" } ] @@ -268,6 +267,7 @@ Maps to the [Location Lookup](lookup.md) capability. "latitude": 40.707, "longitude": -74.011 }, + "amenities": ["curbside_pickup", "in_store_pickup", "parking"], "timezone": "America/New_York" } ], @@ -297,6 +297,10 @@ All application-level outcomes return a successful JSON-RPC result with the UCP ## Entities +### Amenity Type + +{{ schema_fields('types/amenity_type', 'location/mcp') }} + ### Location {: #location-entity } {{ schema_fields('types/location', 'location/mcp') }} @@ -305,10 +309,6 @@ All application-level outcomes return a successful JSON-RPC result with the UCP {{ schema_fields('types/location_filter', 'location/mcp') }} -### Location Offering Filter {: #location-offering-filter-schema } - -{{ schema_fields('types/location_offering_filter', 'location/mcp') }} - ### Error Response {: #error-response } {{ schema_fields('types/error_response', 'location/mcp') }} diff --git a/docs/specification/location/rest.md b/docs/specification/location/rest.md index b59c77a9a..c19cf33a0 100644 --- a/docs/specification/location/rest.md +++ b/docs/specification/location/rest.md @@ -94,9 +94,7 @@ Maps to the [Location Search](search.md) capability. "hours": { "open_now": true }, - "offerings": { - "amenities": ["curbside_pickup"] - }, + "amenities": ["curbside_pickup"], "geo": { "serves": { "point": { @@ -141,6 +139,7 @@ Maps to the [Location Search](search.md) capability. "latitude": 37.420, "longitude": -122.080 }, + "amenities": ["curbside_pickup", "in_store_pickup", "parking"], "timezone": "America/Los_Angeles" } ] @@ -167,14 +166,12 @@ Maps to the [Location Search](search.md) capability. "hours": { "open_now": true }, - "offerings": { - "inventory": [ - { - "id": "item_id_phone_15_pro", - "availability_status": "in_stock" - } - ] - }, + "inventory": [ + { + "id": "item_id_phone_15_pro", + "availability_status": "in_stock" + } + ], "geo": { "distance": { "center": { @@ -219,6 +216,7 @@ Maps to the [Location Search](search.md) capability. "latitude": 40.709, "longitude": -74.008 }, + "amenities": ["curbside_pickup", "in_store_pickup", "parking"], "timezone": "America/New_York" } ] @@ -279,6 +277,7 @@ Maps to the [Location Lookup](lookup.md) capability. "latitude": 40.707, "longitude": -74.011 }, + "amenities": ["curbside_pickup", "in_store_pickup", "parking"], "timezone": "America/New_York", "hours": [ { @@ -341,6 +340,7 @@ Maps to the [Location Lookup](lookup.md) capability. "latitude": 40.790, "longitude": -73.950 }, + "amenities": ["in_store_pickup"], "timezone": "America/New_York", "hours": [ { @@ -431,6 +431,7 @@ Maps to the [Location Lookup](lookup.md) capability. "latitude": 40.707, "longitude": -74.011 }, + "amenities": ["curbside_pickup", "in_store_pickup", "parking"], "timezone": "America/New_York", "hours": [ { @@ -510,6 +511,10 @@ All application-level outcomes return HTTP 200 with the UCP envelope and optiona {{ extension_schema_fields('ucp.json#/$defs/response_catalog_schema', 'location/rest') }} +### Amenity Type + +{{ schema_fields('types/amenity_type', 'location/rest') }} + ### Location {: #location-entity } {{ schema_fields('types/location', 'location/rest') }} @@ -518,10 +523,6 @@ All application-level outcomes return HTTP 200 with the UCP envelope and optiona {{ schema_fields('types/location_filter', 'location/rest') }} -### Location Offering Filter {: #location-offering-filter-schema } - -{{ schema_fields('types/location_offering_filter', 'location/rest') }} - ### Error Response {: #error-response } {{ schema_fields('types/error_response', 'location/rest') }} diff --git a/docs/specification/location/search.md b/docs/specification/location/search.md index dd7cecc2e..89faa0504 100644 --- a/docs/specification/location/search.md +++ b/docs/specification/location/search.md @@ -72,8 +72,8 @@ Filters locations based on their operating hours: Separates static location characteristics from dynamic availability: * **`amenities`** (Array of Strings): Static features or services of the - location (e.g., `free_wifi`, `parking`, `outdoor_seating`, `curbside_pickup`, `vegetarian`). - All specified amenities **MUST** be supported by the location (AND semantic). + location. All specified amenities **MUST** be supported by the location (AND semantic). + See [Amenity Vocabulary](#amenity-vocabulary) for well-known values. * **`inventory`** (Array of Objects): Real-time availability of items/goods at the location. Some industry specific use cases include: * *Shopping*: Checking stock availability for specific products or variants. @@ -81,6 +81,11 @@ Separates static location characteristics from dynamic availability: Each inventory filter requires an `id` (e.g., product/dish ID) and can optionally specify a coarse `availability_status` value. +#### Amenity Vocabulary + +UCP defines an open string vocabulary for amenities via `amenity_type.json` to ensure cross-business +interoperability. Implementations **SHOULD** map their internal features to the well-known types where applicable. + ### Geographic & Geofencing Filter Supports two distinct, industry-agnostic spatial search models: diff --git a/source/schemas/common/types/amenity_type.json b/source/schemas/common/types/amenity_type.json new file mode 100644 index 000000000..ca0f3838e --- /dev/null +++ b/source/schemas/common/types/amenity_type.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/types/amenity_type.json", + "title": "Amenity Type", + "description": "The type of amenity supported by a vertical-agnostic location. Some well-known values are defined below as examples and have standardized semantics; freeform codes are permitted.", + "type": "string", + "examples": [ + "in_store_pickup", + "curbside_pickup", + "drive_through" + ] +} diff --git a/source/schemas/common/types/location.json b/source/schemas/common/types/location.json index 49d07eeb6..757d2647f 100644 --- a/source/schemas/common/types/location.json +++ b/source/schemas/common/types/location.json @@ -38,6 +38,14 @@ "description": "Geographic coordinates and geofence for the location.", "ucp_request": "omit" }, + "amenities": { + "type": "array", + "description": "Static features of the location (e.g., 'free_wifi', 'wheelchair_accessible', 'parking', 'curbside_pickup').", + "items": { + "$ref": "amenity_type.json" + }, + "ucp_request": "omit" + }, "hours": { "type": "array", "description": "Regular weekly operating hours. Support split hours through multiple entries on the same day and overnight hours (open > close). Omission of days implies full day closures.", diff --git a/source/schemas/common/types/location_filter.json b/source/schemas/common/types/location_filter.json index b0d6ab9c3..5a3d75f8a 100644 --- a/source/schemas/common/types/location_filter.json +++ b/source/schemas/common/types/location_filter.json @@ -60,8 +60,19 @@ }, "additionalProperties": false }, - "offerings": { - "$ref": "location_offering_filter.json" + "amenities": { + "type": "array", + "items": { + "$ref": "amenity_type.json" + }, + "description": "Filter by static services, amenities, or capabilities of the location (e.g., 'free_wifi', 'wheelchair_accessible', 'parking', 'curbside_pickup'). Matches locations that have ALL the listed amenities (AND logic)." + }, + "inventory": { + "type": "array", + "items": { + "$ref": "inventory_filter.json" + }, + "description": "Filter by real-time availability of inventory (e.g., retail products or restaurant dishes) at the location. Matches locations that have ALL the listed items in stock (AND logic)." } }, "additionalProperties": true diff --git a/source/schemas/common/types/location_offering_filter.json b/source/schemas/common/types/location_offering_filter.json deleted file mode 100644 index 05c62ec00..000000000 --- a/source/schemas/common/types/location_offering_filter.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://ucp.dev/schemas/common/types/location_offering_filter.json", - "title": "Location Offering Filter", - "description": "Filter criteria for location offerings, separating static amenities/services from dynamic inventory.", - "type": "object", - "properties": { - "amenities": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Filter by static services, amenities, or capabilities of the location (e.g., 'free_wifi', 'wheelchair_accessible', 'parking', 'curbside_pickup'). Matches locations that have ALL the listed amenities (AND logic)." - }, - "inventory": { - "type": "array", - "items": { - "$ref": "inventory_filter.json" - }, - "description": "Filter by real-time availability of inventory (e.g., retail products or restaurant dishes) at the location. Matches locations that have ALL the listed items in stock (AND logic)." - } - }, - "additionalProperties": true -} From 1ff81f3a858d6e260c148e37af61086eddca0110 Mon Sep 17 00:00:00 2001 From: Jing Li Date: Thu, 30 Jul 2026 17:07:00 -0400 Subject: [PATCH 21/37] Address bounded location representation problem and clean up misc/unused ucp annotation for fulfillment related files. --- source/schemas/common/types/context.json | 6 +- source/schemas/common/types/location.json | 102 +++++++----------- .../schemas/common/types/location_base.json | 37 +++++++ .../schemas/shopping/types/fulfillment.json | 1 - .../types/fulfillment_available_method.json | 1 - .../types/fulfillment_destination.json | 3 +- .../shopping/types/fulfillment_option.json | 1 - .../types/fulfillment_option_base.json | 1 - .../shopping/types/shipping_destination.json | 1 - 9 files changed, 82 insertions(+), 71 deletions(-) create mode 100644 source/schemas/common/types/location_base.json diff --git a/source/schemas/common/types/context.json b/source/schemas/common/types/context.json index c62e37548..268369e8a 100644 --- a/source/schemas/common/types/context.json +++ b/source/schemas/common/types/context.json @@ -7,7 +7,7 @@ "additionalProperties": true, "allOf": [ { - "$ref": "../../common/types/locality.json" + "$ref": "locality.json" }, { "type": "object", @@ -30,7 +30,7 @@ "description": "Buyer claims about eligible benefits such as loyalty membership, payment instrument perks, and similar. Recognized claims MAY inform the Business response (e.g., member-only product availability, adjusted pricing in catalog, provisional discounts at cart or checkout). Businesses MUST ignore unrecognized values without error. Values MUST use reverse-domain naming (e.g., 'com.example.loyalty_gold', 'org.school.student') and MUST be non-identifying.", "uniqueItems": true, "items": { - "$ref": "../../common/types/reverse_domain_name.json" + "$ref": "reverse_domain_name.json" } }, "payment": { @@ -41,7 +41,7 @@ "required": ["handler"], "properties": { "handler": { - "$ref": "../../common/types/reverse_domain_name.json", + "$ref": "reverse_domain_name.json", "description": "Handler registry key advertised in the Business profile's `ucp.payment_handlers`." }, "types": { diff --git a/source/schemas/common/types/location.json b/source/schemas/common/types/location.json index 757d2647f..549bec782 100644 --- a/source/schemas/common/types/location.json +++ b/source/schemas/common/types/location.json @@ -2,70 +2,50 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://ucp.dev/schemas/common/types/location.json", "title": "Location", - "description": "A physical location (e.g., store, restaurant, locker, warehouse).", + "description": "The full, rich representation of a physical business location. Builds on top of the Base Location schema to include discovery-centric details such as geographic coordinates, operating hours, timezone, and amenities.", "type": "object", - "required": ["id", "name"], "additionalProperties": true, - "properties": { - "id": { - "type": "string", - "description": "Unique location identifier.", - "ucp_request": { - "transition": { - "from": "omit", - "to": "optional", - "description": "Location ids MAY be specified by platforms in requests." + "allOf": [ + { "$ref": "location_base.json" }, + { + "type": "object", + "properties": { + "geo": { + "$ref": "geo.json", + "description": "Geographic coordinates and geofence for the location.", + "ucp_request": "omit" + }, + "amenities": { + "type": "array", + "description": "Static features of the location (e.g., 'free_wifi', 'wheelchair_accessible', 'parking', 'curbside_pickup').", + "items": { + "$ref": "amenity_type.json" + }, + "ucp_request": "omit" + }, + "hours": { + "type": "array", + "description": "Regular weekly operating hours. Support split hours through multiple entries on the same day and overnight hours (open > close). Omission of days implies full day closures.", + "items": { + "$ref": "daily_hour.json" + }, + "ucp_request": "omit" + }, + "exception_hours": { + "type": "array", + "description": "Exception hours for specific dates (holidays, closures, etc.). Support split hours through multiple entries on the same day and overnight hours (open > close). For full day closures, MUST explicitly set open and close to 00:00.", + "items": { + "$ref": "exception_hour.json" + }, + "ucp_request": "omit" + }, + "timezone": { + "type": "string", + "description": "IANA timezone identifier (e.g., 'America/New_York'). MUST be set when hours or exception_hours are present. Required for correct interpretation.", + "ucp_request": "omit" } - } - }, - "name": { - "type": "string", - "description": "Location display name.", - "ucp_request": { - "transition": { - "from": "required", - "to": "omit", - "description": "Location names should not be specified by platforms." - } - } - }, - "address": { - "$ref": "postal_address.json", - "description": "Physical address of the location." - }, - "geo": { - "$ref": "geo.json", - "description": "Geographic coordinates and geofence for the location.", - "ucp_request": "omit" - }, - "amenities": { - "type": "array", - "description": "Static features of the location (e.g., 'free_wifi', 'wheelchair_accessible', 'parking', 'curbside_pickup').", - "items": { - "$ref": "amenity_type.json" - }, - "ucp_request": "omit" - }, - "hours": { - "type": "array", - "description": "Regular weekly operating hours. Support split hours through multiple entries on the same day and overnight hours (open > close). Omission of days implies full day closures.", - "items": { - "$ref": "daily_hour.json" - }, - "ucp_request": "omit" - }, - "exception_hours": { - "type": "array", - "description": "Exception hours for specific dates (holidays, closures, etc.). Support split hours through multiple entries on the same day and overnight hours (open > close). For full day closures, MUST explicitly set open and close to 00:00.", - "items": { - "$ref": "exception_hour.json" }, - "ucp_request": "omit" - }, - "timezone": { - "type": "string", - "description": "IANA timezone identifier (e.g., 'America/New_York'). MUST be set when hours or exception_hours are present. Required for correct interpretation.", - "ucp_request": "omit" + "additionalProperties": true } - } + ] } diff --git a/source/schemas/common/types/location_base.json b/source/schemas/common/types/location_base.json new file mode 100644 index 000000000..c18d3b1ce --- /dev/null +++ b/source/schemas/common/types/location_base.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/types/location_base.json", + "title": "Base Location", + "description": "A minimalist representation of a physical business location. Contains only the stable identifier, display name, and postal address. Used in contexts (like checkout & catalog lookup) to minimize payload size and avoid exposing unnecessary discovery details.", + "type": "object", + "required": ["id", "name"], + "additionalProperties": true, + "properties": { + "id": { + "type": "string", + "description": "Unique location identifier.", + "ucp_request": { + "transition": { + "from": "omit", + "to": "optional", + "description": "Location ids MAY be specified by platforms in requests." + } + } + }, + "name": { + "type": "string", + "description": "Location display name.", + "ucp_request": { + "transition": { + "from": "required", + "to": "omit", + "description": "Location names should not be specified by platforms." + } + } + }, + "address": { + "$ref": "postal_address.json", + "description": "Physical address of the location." + } + } +} diff --git a/source/schemas/shopping/types/fulfillment.json b/source/schemas/shopping/types/fulfillment.json index 6aa8c0589..1150baf52 100644 --- a/source/schemas/shopping/types/fulfillment.json +++ b/source/schemas/shopping/types/fulfillment.json @@ -4,7 +4,6 @@ "title": "Fulfillment", "description": "Container for fulfillment methods and availability.", "type": "object", - "ucp_shared_request": true, "properties": { "methods": { "type": "array", diff --git a/source/schemas/shopping/types/fulfillment_available_method.json b/source/schemas/shopping/types/fulfillment_available_method.json index 596293d8e..7d1a7a4db 100644 --- a/source/schemas/shopping/types/fulfillment_available_method.json +++ b/source/schemas/shopping/types/fulfillment_available_method.json @@ -4,7 +4,6 @@ "title": "Fulfillment Available Method", "description": "Inventory availability hint for a fulfillment method type.", "type": "object", - "ucp_shared_request": true, "required": ["type", "line_item_ids"], "additionalProperties": true, "properties": { diff --git a/source/schemas/shopping/types/fulfillment_destination.json b/source/schemas/shopping/types/fulfillment_destination.json index 736bb6e93..d9af73dd0 100644 --- a/source/schemas/shopping/types/fulfillment_destination.json +++ b/source/schemas/shopping/types/fulfillment_destination.json @@ -4,13 +4,12 @@ "title": "Fulfillment Destination", "description": "A destination for fulfillment.", "type": "object", - "ucp_shared_request": true, "oneOf": [ { "$ref": "shipping_destination.json" }, { - "$ref": "../../common/types/location.json", + "$ref": "../../common/types/location_base.json", "description": "A pickup retail location (e.g. store, locker, etc.)." } ] diff --git a/source/schemas/shopping/types/fulfillment_option.json b/source/schemas/shopping/types/fulfillment_option.json index 05503be54..788cb2757 100644 --- a/source/schemas/shopping/types/fulfillment_option.json +++ b/source/schemas/shopping/types/fulfillment_option.json @@ -4,7 +4,6 @@ "title": "Fulfillment Option", "description": "A fulfillment option within a group (e.g., Standard Shipping $5, Express $15). Extends the fulfillment option base with cost and timing.", "type": "object", - "ucp_shared_request": true, "allOf": [ { "$ref": "fulfillment_option_base.json" diff --git a/source/schemas/shopping/types/fulfillment_option_base.json b/source/schemas/shopping/types/fulfillment_option_base.json index 1f7367d32..0eeebee77 100644 --- a/source/schemas/shopping/types/fulfillment_option_base.json +++ b/source/schemas/shopping/types/fulfillment_option_base.json @@ -4,7 +4,6 @@ "title": "Fulfillment Option Base", "description": "Common base for a fulfillment option: an addressable, renderable choice (e.g. Standard, Express). Catalog uses this base directly; checkout composes it with cost and timing.", "type": "object", - "ucp_shared_request": true, "required": ["id", "title"], "additionalProperties": true, "properties": { diff --git a/source/schemas/shopping/types/shipping_destination.json b/source/schemas/shopping/types/shipping_destination.json index ec77b9789..6acdf1356 100644 --- a/source/schemas/shopping/types/shipping_destination.json +++ b/source/schemas/shopping/types/shipping_destination.json @@ -4,7 +4,6 @@ "title": "Shipping Destination", "description": "Shipping destination.", "type": "object", - "ucp_shared_request": true, "allOf": [ { "$ref": "../../common/types/postal_address.json" From 0dc7e06fa0b1c15c1d1ff7024b9b1a5cc8a399b4 Mon Sep 17 00:00:00 2001 From: Jing Li Date: Thu, 30 Jul 2026 17:32:33 -0400 Subject: [PATCH 22/37] Fix broken reference rendering. --- source/schemas/common/types/location_base.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/schemas/common/types/location_base.json b/source/schemas/common/types/location_base.json index c18d3b1ce..07975ce24 100644 --- a/source/schemas/common/types/location_base.json +++ b/source/schemas/common/types/location_base.json @@ -1,7 +1,7 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://ucp.dev/schemas/common/types/location_base.json", - "title": "Base Location", + "title": "Location Base", "description": "A minimalist representation of a physical business location. Contains only the stable identifier, display name, and postal address. Used in contexts (like checkout & catalog lookup) to minimize payload size and avoid exposing unnecessary discovery details.", "type": "object", "required": ["id", "name"], From 80145ddf226960397a4bc906a6c7e731a4ed9ec6 Mon Sep 17 00:00:00 2001 From: Jing Li Date: Fri, 31 Jul 2026 11:13:08 -0400 Subject: [PATCH 23/37] Fix example on REST to follow proper exception_hour representation. --- docs/specification/location/rest.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/specification/location/rest.md b/docs/specification/location/rest.md index c19cf33a0..720b99297 100644 --- a/docs/specification/location/rest.md +++ b/docs/specification/location/rest.md @@ -319,7 +319,7 @@ Maps to the [Location Lookup](lookup.md) capability. "exception_hours": [ { "from": "2026-11-26", - "through": "2026-11-26", + "through": "2026-11-27", "label": "Thanksgiving", "open": "00:00", "close": "00:00" @@ -372,7 +372,7 @@ Maps to the [Location Lookup](lookup.md) capability. "exception_hours": [ { "from": "2026-11-26", - "through": "2026-11-26", + "through": "2026-11-27", "label": "Thanksgiving", "open": "00:00", "close": "00:00" @@ -468,7 +468,7 @@ Maps to the [Location Lookup](lookup.md) capability. "exception_hours": [ { "from": "2026-11-26", - "through": "2026-11-26", + "through": "2026-11-27", "label": "Thanksgiving", "open": "00:00", "close": "00:00" From 675ec064637b5013a279e5a68f2ee8207d222595 Mon Sep 17 00:00:00 2001 From: Jing Li Date: Fri, 31 Jul 2026 21:10:01 -0400 Subject: [PATCH 24/37] Clean up legacy field description. --- source/schemas/common/types/exception_hour.json | 2 +- source/schemas/common/types/location_filter.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/source/schemas/common/types/exception_hour.json b/source/schemas/common/types/exception_hour.json index 09b2dfdf9..2b8cc6d9e 100644 --- a/source/schemas/common/types/exception_hour.json +++ b/source/schemas/common/types/exception_hour.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://ucp.dev/schemas/common/types/exception_hour.json", "title": "Exception Hour", - "description": "Operating hours for a specific date range that differs from normal ones (e.g., holiday or temporary change). Omission of explicit intervals implies open 24 hours.", + "description": "Operating hours for a specific date range that differs from normal ones (e.g., holiday or temporary change).", "type": "object", "required": ["from", "through"], "allOf": [ diff --git a/source/schemas/common/types/location_filter.json b/source/schemas/common/types/location_filter.json index 5a3d75f8a..db25ed0d5 100644 --- a/source/schemas/common/types/location_filter.json +++ b/source/schemas/common/types/location_filter.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://ucp.dev/schemas/common/types/location_filter.json", "title": "Location Filter", - "description": "Filter criteria to narrow location search/lookup results. All specified filters combine with AND logic.", + "description": "Filter criteria to narrow location search/lookup results. All specified outermost filters combine with AND logic.", "type": "object", "properties": { "geo": { From 17dac60cf5896b81b07a33e3f9deac1fecb76598 Mon Sep 17 00:00:00 2001 From: Jing Li Date: Fri, 31 Jul 2026 22:36:35 -0400 Subject: [PATCH 25/37] Fix signature definition in MCP JSONRPC transport schema definition. --- source/services/common/mcp.openrpc.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/source/services/common/mcp.openrpc.json b/source/services/common/mcp.openrpc.json index 4efbde845..5c7518b1d 100644 --- a/source/services/common/mcp.openrpc.json +++ b/source/services/common/mcp.openrpc.json @@ -44,7 +44,15 @@ }, "signature": { "type": "string", - "description": "Detached JWS signature in format `..` for message integrity." + "description": "RFC 9421 HTTP message signature." + }, + "signature_input": { + "type": "string", + "description": "RFC 9421 Signature-Input header. Format: `sig1=(\"@method\" \"@path\" ...);created=;keyid=\"\"`." + }, + "content-digest": { + "type": "string", + "description": "Body digest per RFC 9530. Format: `sha-256=::`." } } } From 5dfcbf7304c95dedc6a3c9283b461eec02bf1611 Mon Sep 17 00:00:00 2001 From: Jing Li Date: Fri, 31 Jul 2026 22:50:06 -0400 Subject: [PATCH 26/37] Add validation rule for timezone in location.json. --- source/schemas/common/types/location.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/source/schemas/common/types/location.json b/source/schemas/common/types/location.json index 549bec782..1e3def5f9 100644 --- a/source/schemas/common/types/location.json +++ b/source/schemas/common/types/location.json @@ -47,5 +47,14 @@ }, "additionalProperties": true } - ] + ], + "if": { + "anyOf": [ + { "required": ["hours"] }, + { "required": ["exception_hours"] } + ] + }, + "then": { + "required": ["timezone"] + } } From 07f2051e25b06004081a66e4c240adcad831d953 Mon Sep 17 00:00:00 2001 From: Jing Li Date: Fri, 31 Jul 2026 23:14:39 -0400 Subject: [PATCH 27/37] Loosen language on what is being considered as a search inputs to give more flexible combination of request inputs. --- docs/specification/location/search.md | 8 ++++---- source/schemas/common/types/location.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/specification/location/search.md b/docs/specification/location/search.md index 89faa0504..b8180eb51 100644 --- a/docs/specification/location/search.md +++ b/docs/specification/location/search.md @@ -39,10 +39,10 @@ amenities and inventory availability. ## Search Inputs -A valid search request **MUST** include at least one of: a `query` string -or one or more `filters`. When `query` is omitted, the request represents -a browse operation — the business returns locations matching the provided -filters without text-relevance ranking. +A valid search request **MUST** include at least one of: a `query` string, one or more +`filters`, platform-provided user `context` hints, or an extension-defined input. +When `query` is omitted, the request represents a browse operation — the business +returns locations matching the provided filters without text-relevance ranking. Implementations **MUST** validate that incoming requests contain at least one recognized input and **SHOULD** reject empty or invalid requests with an diff --git a/source/schemas/common/types/location.json b/source/schemas/common/types/location.json index 1e3def5f9..a8b21ad6a 100644 --- a/source/schemas/common/types/location.json +++ b/source/schemas/common/types/location.json @@ -12,7 +12,7 @@ "properties": { "geo": { "$ref": "geo.json", - "description": "Geographic coordinates and geofence for the location.", + "description": "Geographic coordinates for the location.", "ucp_request": "omit" }, "amenities": { From 41a47e697945e492dd52cdb689995ce08b892244 Mon Sep 17 00:00:00 2001 From: Jing Li Date: Fri, 31 Jul 2026 23:32:07 -0400 Subject: [PATCH 28/37] Add some implementation guidance on how to deal with context-only search requests. --- docs/specification/location/search.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/specification/location/search.md b/docs/specification/location/search.md index b8180eb51..7de40b521 100644 --- a/docs/specification/location/search.md +++ b/docs/specification/location/search.md @@ -50,6 +50,16 @@ appropriate error. Implementations define and enforce their own rules for input presence and content — for example, requiring `query`, rejecting empty `query` strings, or accepting filter-only requests. +> **Implementation guidance:** For processing search requests containing only `context`, +> the following rules **MAY** be followed by businesses: +> +> If the provided `context` is insufficient to determine a location boundary +> (e.g., only country is provided, or context is empty), business **MAY** return a default +> set of locations (e.g., featured locations, or all locations up to a default server-side limit). +> +> If the server cannot resolve the location and does not support default lists, +> it **SHOULD** return an empty list. + ## Search Filters Location filters allow narrowing results based on specific criteria. From 054a7636f6e0ce1ed37e656902a1105227a7c8a0 Mon Sep 17 00:00:00 2001 From: Jing Li Date: Fri, 31 Jul 2026 23:48:52 -0400 Subject: [PATCH 29/37] Add more validation on the filter schema and tighten up prose around how business should handle contextual hints fallback. --- docs/specification/location/search.md | 16 ++++++++++------ source/schemas/common/types/location_filter.json | 6 +++++- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/docs/specification/location/search.md b/docs/specification/location/search.md index 7de40b521..3aa4c0e20 100644 --- a/docs/specification/location/search.md +++ b/docs/specification/location/search.md @@ -55,10 +55,10 @@ empty `query` strings, or accepting filter-only requests. > > If the provided `context` is insufficient to determine a location boundary > (e.g., only country is provided, or context is empty), business **MAY** return a default -> set of locations (e.g., featured locations, or all locations up to a default server-side limit). +> set of locations (e.g., featured locations, or all locations up to a default server-side +> limit) or an empty list. > -> If the server cannot resolve the location and does not support default lists, -> it **SHOULD** return an empty list. +> If the server cannot resolve the location, it **SHOULD** return an error message. ## Search Filters @@ -103,9 +103,11 @@ Supports two distinct, industry-agnostic spatial search models: * **`distance` (Proximity Search)**: Filters for locations within a `max_distance` (in RFC 7035 distance units = meters) of a `center` point. -> **Privacy Integration**: If `center` is omitted, the server **MUST** use the +> **Privacy Integration**: If `center` is omitted, business **MUST** use the > user's address hint provided in the request `context` (which may be coarse/sanitized) -> to derive the center. +> to derive the center. If `context` is omitted or business is unable to resolve the +> provided address hint, then business **MUST** return an error message with +> `code: "location_geo_filter_resolution_failed"`. * **`serves` (Service Area Coverage)**: Filters for locations that can serve a target destination. The business evaluates coverage using their internal service area rules @@ -114,7 +116,9 @@ Supports two distinct, industry-agnostic spatial search models: > **Contextual Fallback**: If the `serves` filter is not explicitly specified in > the request, the business **MAY** use the user's contextual location hints passed in the > request `context` object to implicitly apply a `serves` filter, returning only locations -> that can service the user. +> that can service the user. Similarly to the callout above, if `context` is omitted or +> business is unable to resolve the provided address hint, then business **MUST** return +> an error message with `code: "location_geo_filter_resolution_failed"`. ## Pagination diff --git a/source/schemas/common/types/location_filter.json b/source/schemas/common/types/location_filter.json index db25ed0d5..bc31f999e 100644 --- a/source/schemas/common/types/location_filter.json +++ b/source/schemas/common/types/location_filter.json @@ -39,7 +39,11 @@ "description": "Coarse postal address reference of the target destination (e.g., for ZIP-code based routing)." } }, - "additionalProperties": false + "additionalProperties": false, + "oneOf": [ + { "required": ["point"] }, + { "required": ["address"] } + ] } }, "additionalProperties": false From 8b9d8b3792ee18c300a5493a95da6feeb966ee5c Mon Sep 17 00:00:00 2001 From: Ilya Grigorik Date: Wed, 12 Aug 2026 16:17:27 -0700 Subject: [PATCH 30/37] fix!: define deterministic operating hours for Location service (#687) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * define deterministic operating hours The current Location PR introduces weekly and exceptional operating hours, but leaves several wire and evaluation semantics ambiguous. In particular, closures rely on an artificial midnight interval, `open_now` depends on an implicit server clock, exception date bounds are unclear, and the specification does not define timezone, overnight, DST, overlap, or precedence behavior. Close those gaps with a UCP-native schedule model informed by Schema.org's OpeningHoursSpecification: https://schema.org/OpeningHoursSpecification Schema.org is design input only. UCP owns the field names, values, and evaluation rules defined here. Make weekly intervals explicit and reusable: "hours": [ { "day": "tuesday", "opens": "09:00", "closes": "12:00" }, { "day": "tuesday", "opens": "13:00", "closes": "21:00" } ] Rename `open` and `close` to `opens` and `closes`, and define `day` as a stable UCP weekday identifier rather than localized display text. Multiple entries for one day represent split shifts, and an interval whose closing time is earlier than its opening time continues into the next local date. Refactor the shared time interval schema so `opens` and `closes` are an optional but inseparable pair. Weekly hours require both fields, while exception hours may omit both to represent a full closure. Reject the ambiguous `00:00` to `00:00` pair and reserve `00:00` to `23:59` as the full-local-day sentinel. Replace the previous exception shape: { "from": "2026-11-26", "through": "2026-11-27", "label": "Thanksgiving", "open": "00:00", "close": "00:00" } with inclusive local-date bounds and an actual closure representation: { "title": "Thanksgiving", "valid_from": "2026-11-26", "valid_through": "2026-11-26" } Rename `from`, `through`, and `label` to `valid_from`, `valid_through`, and `title`. Treat `title` as optional presentation metadata that does not affect schedule evaluation. Allow timed exceptions with paired `opens` and `closes`, including multiple entries with identical bounds for split shifts. Define every returned schedule in the Location's Business-owned IANA timezone. Require `timezone` whenever regular or exception hours are present, and keep the canonical schedule independent of the requesting Platform or Buyer's timezone. Specify deterministic evaluation: - convert an exact instant into each Location's local date, weekday, and time - use half-open timed intervals, except for the reserved full-day sentinel - let overnight intervals carry into the following local date - replace regular hours with exception hours at local midnight - treat omitted weekdays as having no interval starting that day - treat absent schedules as unknown rather than closed - evaluate DST gaps and folds pointwise without shifting nonexistent times - reject equal time pairs and intersecting non-identical exception ranges as Business conformance errors where JSON Schema cannot express the constraint Remove the redundant `open_now` filter. It makes results depend on an implicit processing clock and creates undefined precedence when combined with `open_at`. Require one caller-supplied RFC 3339 instant instead: "filters": { "hours": { "open_at": "2026-05-18T17:00:00Z" } } Require `open_at` to include `Z` or a numeric offset. The offset identifies the instant only; the Business still evaluates that instant using each candidate Location's authoritative IANA timezone. Keep the nested hours filter open so extensions can add qualifiers without changing the standard predicate. Move complete Search and Lookup examples into the transport-neutral capability documents. Cover hours with serviceability and amenities, inventory with distance, split shifts, full closures, and partial Lookup success there. Reduce REST and MCP examples to equivalent binding envelopes that link to the same canonical payload examples. This keeps both transports on equal footing, avoids duplicating domain semantics, and prevents one binding's examples from becoming more complete or authoritative than the other. Preserve MCP's required `meta["ucp-agent"].profile` contract while separating protocol metadata from the Location request. This is a breaking correction to the Location PR's draft wire shape: - `open` becomes `opens` - `close` becomes `closes` - `from` becomes `valid_from` - `through` becomes `valid_through` - `label` becomes `title` - `open_now` is removed - full closures omit both time fields instead of using `00:00` to `00:00` * s/weekday/day of week * clarify operating-hours semantics Define `open_at` as the caller-selected instant relevant to the request, such as an expected arrival or pickup time. This avoids framing it as a request for the Business's receipt-time notion of "now": normal request latency does not change the question, and the Business evaluates the supplied instant against each Location's schedule. Describe operating hours more directly as local dates and clock times interpreted using the Location's IANA timezone. Clarify that temporary closures retain the regular `hours` schedule and override it with a date-bounded `exception_hours` entry that omits `opens` and `closes`. Mirror omitted-schedule semantics in the Location schema for implementers who read generated references: - an omitted day has no regular interval beginning that day - an interval from the preceding day may still carry into it - omission of the entire `hours` property means the schedule is unknown Make `time_interval` genuinely reusable by limiting it to generic `HH:MM` opening and closing fields. Location-specific recurrence and timezone interpretation remain with the containing daily, exception, and Location schemas. Remove the schema check that rejected only `00:00`–`00:00`. The actual authoring rule rejects every pair where `opens` equals `closes`, but standard JSON Schema cannot compare sibling values; enforcing one special case would misleadingly imply that other equal pairs are valid. Continue enforcing paired field presence and time formatting mechanically, while keeping unequal times as a normative Business conformance requirement and requiring Platforms not to infer openness from invalid schedule data. * define authority for hours filtering The TC discussion converged on keeping one `open_at` filter, but left open whether both the Platform and Business could apply timing tolerance when interpreting immediate intent. After further consideration, assign that flexibility to one side only. The Platform owns the interpretation of Buyer intent and selects the instant to query. It may use its current time, choose an expected arrival, pickup, or order-acceptance time, and round or adjust that choice to the granularity appropriate to the interaction. Once encoded, however, `open_at` identifies one specific RFC 3339 instant. Require the Business to evaluate that instant exactly as supplied using each Location's authoritative timezone. It must not round, shift, substitute request receipt time, or otherwise reinterpret the value. Allowing both parties to apply independent tolerance would make the evaluated question unknowable and could produce different matches for identical requests near an opening or closing boundary. Apply normal positive-match filter semantics: return a Location only when the Business can establish that it is open at `open_at`. Missing, invalid, out-of-range, or otherwise unusable schedule data is a non-match rather than a reason to guess or adjust the requested instant. Clarify that the numeric offset in `open_at` identifies the queried instant, not the Location's timezone. The Business converts that instant using the Location's authoritative IANA timezone before evaluating its local schedule. State closing-boundary behavior concretely: a `10:00`–`17:00` interval is open immediately before `17:00` and closed at `17:00`. This avoids ambiguity over whether `HH:MM` values represent exact boundaries or minute-sized buckets. Keep exception payloads useful for planning without accumulating stale history. Businesses should remove entries once they cannot affect any current or future instant and publish known future exceptions through the horizon for which their schedule is authoritative. Remove the request-language localization recommendation for exception `title`. The field remains optional presentation metadata, but this capability does not define a localization guarantee for it. --- docs/specification/location/index.md | 118 +++++- docs/specification/location/lookup.md | 127 ++++++- docs/specification/location/mcp.md | 109 +----- docs/specification/location/rest.md | 342 +----------------- docs/specification/location/search.md | 164 ++++++++- source/schemas/common/types/daily_hour.json | 8 +- .../schemas/common/types/exception_hour.json | 21 +- source/schemas/common/types/location.json | 9 +- .../schemas/common/types/location_filter.json | 13 +- .../schemas/common/types/time_interval.json | 17 +- 10 files changed, 459 insertions(+), 469 deletions(-) diff --git a/docs/specification/location/index.md b/docs/specification/location/index.md index b04fb0ddd..943542f29 100644 --- a/docs/specification/location/index.md +++ b/docs/specification/location/index.md @@ -47,8 +47,9 @@ This is vertical-agnostic and enables key commerce flows such as: This is used to determine if a location can serve a specific user (e.g., delivery area check). Clients can perform proximity searches (`distance` filter) or coverage checks (`geofence_point` filter) using the filter. -* **Operating Hours**: Weekly schedules (`hours`) and date-specific overrides - (`exception_hours` - e.g., holidays, temporary closures) associated with a timezone. +* **Operating Hours**: Regular weekly schedules (`hours`) and date-specific + exceptions (`exception_hours`), interpreted in the Location's `timezone`. + See [Operating Hours](#operating-hours). ### Relationship to Other Capabilities @@ -71,6 +72,119 @@ other capabilities (like Catalog, Cart, and Checkout in Shopping): **MUST** be negotiated and finalized authoritatively. Discovery signals **SHOULD NOT** be cached or reused across sessions without re-validation. +## Operating Hours + +### Representation + +The hours filter's [`open_at`](search.md#hours-based-filter) value is an exact +instant. Operating hours use the Location's local date and clock time, +interpreted using its IANA timezone. + +* `hours` is a list of regular weekly intervals. Each item contains `day`, + `opens`, and `closes`. `day` is a stable UCP day-of-week identifier for the + day on which the interval begins, not localized display text. A Platform + **MAY** localize it for presentation. Times use 24-hour `HH:MM` form. +* `exception_hours` is a list of date-specific timed intervals or full + closures. Each item contains inclusive local-date bounds `valid_from` and + `valid_through` in `YYYY-MM-DD` form; equal bounds select one date. Both + `opens` and `closes` define a timed interval, while omitting both defines a + full closure. The optional `title` is a short, human-readable heading. It is + presentation metadata and does not affect schedule evaluation. +* `timezone` identifies the Business-owned canonical local civil-time frame for + both schedules. + +### Evaluation + +Timed intervals are open at `opens` and closed at `closes`. For example, +`10:00`–`17:00` is open immediately before `17:00` and closed at `17:00`. If +`closes` is earlier than `opens`, the interval continues into the next local +date. As a reserved exception to this closing-boundary rule, the exact +`00:00`–`23:59` pair represents the entire local civil date, including daylight +saving time transitions. Every pair with equal `opens` and `closes` is invalid, +including `00:00`–`00:00`. + +Schedule evaluation is deterministic for each instant: convert the instant to +the Location's local date, day of week, and time using `timezone`, then apply the +effective schedule to those local values. During a forward daylight saving time +(DST) transition, local clock labels in the gap correspond to no instants and +are not shifted. During a backward DST transition, both instants in the fold +that map to the same repeated local time receive the same schedule result. The +current schedule shape cannot distinguish the two fold occurrences. + +Multiple `hours` items for the same `day` combine as split shifts. An omitted +day means no regular interval begins that day, but an interval from the +preceding day can carry into it. Absent `hours` means the regular schedule is +unknown, not closed. To represent a temporary full closure, a Business retains +its regular `hours` and adds an `exception_hours` entry for the affected dates +without `opens` or `closes`. + +An exception schedule replaces the regular schedule on every covered local +date. If an interval carries into a local date governed by an exception, that +exception takes authority at local midnight. Intersecting non-identical +exception ranges are invalid, including when one contains another. Timed +entries with identical bounds can coexist as split shifts, but a full closure +stands alone for its bounds. Array order establishes no precedence. + +#### Exception hours example + + +```json +{ + "id": "loc_downtown", + "name": "Downtown Store", + "timezone": "America/New_York", + "exception_hours": [ + { + "title": "Holiday hours", + "valid_from": "2026-12-24", + "valid_through": "2026-12-26", + "opens": "10:00", + "closes": "14:00" + }, + { + "title": "Holiday hours", + "valid_from": "2026-12-24", + "valid_through": "2026-12-26", + "opens": "16:00", + "closes": "18:00" + } + ] +} +``` + +The two entries share date bounds, so they define split shifts that apply +independently on each date in the inclusive range. A full closure instead uses +one item that omits both `opens` and `closes`. + +### Guidelines + +#### Business + +A Business owns each Location's canonical schedule frame. When a Business +emits `hours` or `exception_hours`, it **MUST** express every `day`, `opens`, +`closes`, `valid_from`, and `valid_through` value in that frame and **MUST** +include `timezone` as a valid +[Internet Assigned Numbers Authority (IANA) Time Zone Database](https://www.iana.org/time-zones) +identifier. A Business **MUST NOT** vary the canonical schedule frame according +to the requesting Platform's or Buyer's timezone. Because JSON Schema does not +enforce every semantic constraint, a Business **MUST** emit only schedules that +follow the rules above, including unequal `opens` and `closes`, a `valid_from` +value no later than `valid_through`, and valid exception-range intersections. +A Business **SHOULD** omit an `exception_hours` entry once it can no longer +affect the schedule at any current or future instant. It **SHOULD** publish +known future exceptions through the planning horizon for which its schedule is +authoritative. + +#### Platform + +When evaluating a returned schedule, a Platform **MUST** use the returned +Location's `timezone`. A Platform **MAY** convert concrete dated occurrences to +another timezone for presentation, but it **MUST NOT** reinterpret the canonical +schedule values in another timezone. A Platform **MUST NOT** infer that a +Location with absent, invalid, or otherwise unusable schedule data is open. A +Platform **MAY** present `title` according to its presentation policy and +**MUST NOT** use it to determine whether a Location is open or closed. + ## Shared Entities ### Context diff --git a/docs/specification/location/lookup.md b/docs/specification/location/lookup.md index be1fbfc7c..de4bd050f 100644 --- a/docs/specification/location/lookup.md +++ b/docs/specification/location/lookup.md @@ -55,12 +55,14 @@ Optional `filters` (hours, offerings/inventory, geo) are accepted to narrow down the returned locations. Filters use the same schema and AND semantics as [Search Filters](search.md#search-filters). -Filters apply **after** identifier resolution. For example, if a client requests -`["loc_downtown", "loc_uptown"]` with a filter of `open_now: true`: +Filters apply **after** identifier resolution. For example, if a Platform +requests `["loc_downtown", "loc_uptown"]` with an hours filter of +`{"open_at": "2026-05-18T17:00:00Z"}`: -1. The server first resolves both identifiers to their respective locations. -2. The server then evaluates the `open_now` hour filter against each resolved location. -3. If `loc_uptown` is currently closed, it is excluded, and only `loc_downtown` is returned. +1. The Business first resolves both identifiers to their respective Locations. +2. The Business evaluates the supplied instant against each resolved Location. +3. If `loc_uptown` is closed at that instant, the Business excludes it and + returns only `loc_downtown`. ### Request @@ -70,6 +72,121 @@ Filters apply **after** identifier resolution. For example, if a client requests {{ extension_schema_fields('location_lookup.json#/$defs/lookup_response', 'location') }} +## Examples {: #examples } + +The following request and response are transport-neutral UCP payloads. + +### Downtown Store schedule + +=== "Request" + + + ```json + { + "ids": ["loc_downtown"] + } + ``` + +=== "Response" + + + ```json + { + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.common.location.lookup": [ + {"version": "{{ ucp_version }}"} + ] + } + }, + "locations": [ + { + "id": "loc_downtown", + "name": "Downtown Store", + "address": { + "street_address": "100 Broadway", + "address_locality": "New York", + "address_region": "NY", + "address_country": "US", + "postal_code": "10005" + }, + "geo": { + "latitude": 40.707, + "longitude": -74.011 + }, + "amenities": ["curbside_pickup", "in_store_pickup", "parking"], + "timezone": "America/New_York", + "hours": [ + {"day": "monday", "opens": "09:00", "closes": "21:00"}, + {"day": "tuesday", "opens": "09:00", "closes": "12:00"}, + {"day": "tuesday", "opens": "13:00", "closes": "21:00"}, + {"day": "wednesday", "opens": "09:00", "closes": "21:00"}, + {"day": "thursday", "opens": "09:00", "closes": "21:00"}, + {"day": "friday", "opens": "09:00", "closes": "22:00"}, + {"day": "saturday", "opens": "10:00", "closes": "20:00"} + ], + "exception_hours": [ + { + "title": "Thanksgiving", + "valid_from": "2026-11-26", + "valid_through": "2026-11-26" + } + ] + } + ] + } + ``` + +Tuesday's two `hours` entries form a split shift. Sunday has no `hours` entry, +meaning no regular interval begins that day. The `exception_hours` entry omits +`opens` and `closes`, making it a full closure. +See [Operating Hours](index.md#operating-hours) for schedule representation and +evaluation rules. + +### Partial success + +=== "Request" + + + ```json + { + "ids": ["loc_downtown", "loc_invalid_id"] + } + ``` + +=== "Response" + + + ```json + { + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.common.location.lookup": [ + {"version": "{{ ucp_version }}"} + ] + } + }, + "locations": [ + { + "id": "loc_downtown", + "name": "Downtown Store" + } + ], + "messages": [ + { + "type": "info", + "code": "not_found", + "content": "Unable to find the location associated with loc_invalid_id." + } + ] + } + ``` + +The request succeeds with the Locations that resolve. A Business can use an +informational `not_found` message to identify an unresolved ID. + ## Transport Bindings * [REST Binding](rest.md#post-locationslookup): `POST /locations/lookup` diff --git a/docs/specification/location/mcp.md b/docs/specification/location/mcp.md index 4f0217810..dbb7aeea1 100644 --- a/docs/specification/location/mcp.md +++ b/docs/specification/location/mcp.md @@ -59,38 +59,11 @@ Businesses advertise MCP transport availability for the Common service and Locat ### Request Metadata -MCP clients **MUST** include a `meta` object in every request containing -protocol metadata: - - -```json -{ - "jsonrpc": "2.0", - "id": 1, - "method": "tools/call", - "params": { - "name": "search_locations", - "arguments": { - "meta": { - "ucp-agent": { - "profile": "https://platform.example/profiles/v2026-01/agent.json" - } - }, - "location": { - "query": "grocery store open now", - "filters": { - "hours": { - "open_now": true - } - } - } - } - } -} -``` - -The `meta["ucp-agent"]` field is **required** on all requests to enable -version compatibility checking and capability negotiation. +A Platform using MCP **MUST** include a `meta` object with +`meta["ucp-agent"].profile` in every request. The field identifies the +Platform's UCP profile for version compatibility checks and capability +negotiation. Protocol metadata remains in `meta`, separate from the domain +request in `location`. ## Tools @@ -101,7 +74,8 @@ version compatibility checking and capability negotiation. ### `search_locations` -Maps to the [Location Search](search.md) capability. +Maps to the [Location Search](search.md) capability. See the +[complete transport-neutral Search example](search.md#examples). #### Request Arguments @@ -111,7 +85,7 @@ Maps to the [Location Search](search.md) capability. {{ extension_schema_fields('location_search.json#/$defs/search_response', 'location/mcp') }} -#### Example +#### Binding envelope example === "Request" @@ -130,26 +104,7 @@ Maps to the [Location Search](search.md) capability. } }, "location": { - "query": "grocery store near me", - "context": { - "address_country": "US", - "address_region": "CA", - "postal_code": "94043" - }, - "filters": { - "hours": { - "open_now": true - }, - "amenities": ["curbside_pickup"], - "geo": { - "serves": { - "point": { - "latitude": 37.422, - "longitude": -122.084 - } - } - } - } + "query": "grocery store" } } } @@ -176,20 +131,7 @@ Maps to the [Location Search](search.md) capability. "locations": [ { "id": "loc_valley_grocers", - "name": "Valley Grocers", - "address": { - "street_address": "789 Maple Ave", - "address_locality": "Mountain View", - "address_region": "CA", - "address_country": "US", - "postal_code": "94043" - }, - "geo": { - "latitude": 37.420, - "longitude": -122.080 - }, - "amenities": ["curbside_pickup", "in_store_pickup", "parking"], - "timezone": "America/Los_Angeles" + "name": "Valley Grocers" } ] } @@ -199,7 +141,8 @@ Maps to the [Location Search](search.md) capability. ### `lookup_locations` -Maps to the [Location Lookup](lookup.md) capability. +Maps to the [Location Lookup](lookup.md) capability. See the +[complete transport-neutral Lookup example](lookup.md#examples). #### Request Arguments @@ -209,11 +152,11 @@ Maps to the [Location Lookup](lookup.md) capability. {{ extension_schema_fields('location_lookup.json#/$defs/lookup_response', 'location/mcp') }} -#### Example +#### Binding envelope example === "Request" - + ```json { "jsonrpc": "2.0", @@ -228,7 +171,7 @@ Maps to the [Location Lookup](lookup.md) capability. } }, "location": { - "ids": ["loc_downtown", "loc_uptown"] + "ids": ["loc_downtown"] } } } @@ -255,27 +198,7 @@ Maps to the [Location Lookup](lookup.md) capability. "locations": [ { "id": "loc_downtown", - "name": "Downtown Store", - "address": { - "street_address": "100 Broadway", - "address_locality": "New York", - "address_region": "NY", - "address_country": "US", - "postal_code": "10005" - }, - "geo": { - "latitude": 40.707, - "longitude": -74.011 - }, - "amenities": ["curbside_pickup", "in_store_pickup", "parking"], - "timezone": "America/New_York" - } - ], - "messages": [ - { - "type": "info", - "code": "not_found", - "content": "Unable to find the location associated with loc_uptown" + "name": "Downtown Store" } ] } diff --git a/docs/specification/location/rest.md b/docs/specification/location/rest.md index 720b99297..366830698 100644 --- a/docs/specification/location/rest.md +++ b/docs/specification/location/rest.md @@ -67,11 +67,12 @@ Location capabilities through their UCP profile at `/.well-known/ucp`. ### `POST /locations/search` -Maps to the [Location Search](search.md) capability. +Maps to the [Location Search](search.md) capability. See the +[complete transport-neutral Search example](search.md#examples). {{ method_fields('search_locations', 'common/rest.openapi.json', 'location/rest') }} -#### Example: Search for Grocery Stores with Local Delivery Coverage (Geofencing) +#### Binding envelope example === "Request" @@ -84,26 +85,7 @@ Maps to the [Location Search](search.md) capability. UCP-Agent: profile="https://platform.example/profiles/v2026-01/agent.json" { - "query": "grocery store near me", - "context": { - "address_country": "US", - "address_region": "CA", - "postal_code": "94043" - }, - "filters": { - "hours": { - "open_now": true - }, - "amenities": ["curbside_pickup"], - "geo": { - "serves": { - "point": { - "latitude": 37.422, - "longitude": -122.084 - } - } - } - } + "query": "grocery store" } ``` @@ -127,97 +109,7 @@ Maps to the [Location Search](search.md) capability. "locations": [ { "id": "loc_valley_grocers", - "name": "Valley Grocers", - "address": { - "street_address": "789 Maple Ave", - "address_locality": "Mountain View", - "address_region": "CA", - "address_country": "US", - "postal_code": "94043" - }, - "geo": { - "latitude": 37.420, - "longitude": -122.080 - }, - "amenities": ["curbside_pickup", "in_store_pickup", "parking"], - "timezone": "America/Los_Angeles" - } - ] - } - ``` - -#### Example: Search for Electronics Stores with Phone In-stock (Store Finder) - -=== "Request" - - - ```json - POST /locations/search HTTP/1.1 - Host: business.example.com - Content-Type: application/json - Request-Id: 9ef9b0c2-78d1-4e4b-91c2-3e2ef0d3ab9f - UCP-Agent: profile="https://platform.example/profiles/v2026-01/agent.json" - - { - "context": { - "address_country": "US" - }, - "filters": { - "hours": { - "open_now": true - }, - "inventory": [ - { - "id": "item_id_phone_15_pro", - "availability_status": "in_stock" - } - ], - "geo": { - "distance": { - "center": { - "latitude": 40.707, - "longitude": -74.011 - }, - "max_distance": 10000 - } - } - } - } - ``` - -=== "Response" - - - ```json - HTTP/1.1 200 OK - Content-Type: application/json - - { - "ucp": { - "version": "{{ ucp_version }}", - "capabilities": { - "dev.ucp.common.location.search": [ - {"version": "{{ ucp_version }}"} - ] - } - }, - "locations": [ - { - "id": "loc_downtown_electronics", - "name": "Downtown Electronics", - "address": { - "street_address": "100 Broadway", - "address_locality": "New York", - "address_region": "NY", - "address_country": "US", - "postal_code": "10005" - }, - "geo": { - "latitude": 40.709, - "longitude": -74.008 - }, - "amenities": ["curbside_pickup", "in_store_pickup", "parking"], - "timezone": "America/New_York" + "name": "Valley Grocers" } ] } @@ -225,165 +117,12 @@ Maps to the [Location Search](search.md) capability. ### `POST /locations/lookup` -Maps to the [Location Lookup](lookup.md) capability. +Maps to the [Location Lookup](lookup.md) capability. See the +[complete transport-neutral Lookup example](lookup.md#examples). {{ method_fields('lookup_locations', 'common/rest.openapi.json', 'location/rest') }} -#### Example: Simple Lookup - -=== "Request" - - - ```json - POST /locations/lookup HTTP/1.1 - Host: business.example.com - Content-Type: application/json - Request-Id: 2c9b0c2a-18d1-4e4b-91c2-3e2ef0d3ab9f - UCP-Agent: profile="https://platform.example/profiles/v2026-01/agent.json" - - { - "ids": ["loc_downtown", "loc_uptown"] - } - ``` - -=== "Response" - - - ```json - HTTP/1.1 200 OK - Content-Type: application/json - - { - "ucp": { - "version": "{{ ucp_version }}", - "capabilities": { - "dev.ucp.common.location.lookup": [ - {"version": "{{ ucp_version }}"} - ] - } - }, - "locations": [ - { - "id": "loc_downtown", - "name": "Downtown Store", - "address": { - "street_address": "100 Broadway", - "address_locality": "New York", - "address_region": "NY", - "address_country": "US", - "postal_code": "10005" - }, - "geo": { - "latitude": 40.707, - "longitude": -74.011 - }, - "amenities": ["curbside_pickup", "in_store_pickup", "parking"], - "timezone": "America/New_York", - "hours": [ - { - "day": "monday", - "open": "09:00", - "close": "21:00" - }, - { - "day": "tuesday", - "open": "09:00", - "close": "12:00" - }, - { - "day": "tuesday", - "open": "13:00", - "close": "21:00" - }, - { - "day": "wednesday", - "open": "09:00", - "close": "21:00" - }, - { - "day": "thursday", - "open": "09:00", - "close": "21:00" - }, - { - "day": "friday", - "open": "09:00", - "close": "22:00" - }, - { - "day": "saturday", - "open": "10:00", - "close": "20:00" - } - ], - "exception_hours": [ - { - "from": "2026-11-26", - "through": "2026-11-27", - "label": "Thanksgiving", - "open": "00:00", - "close": "00:00" - } - ] - }, - { - "id": "loc_uptown", - "name": "Uptown Boutique", - "address": { - "street_address": "2000 Madison Ave", - "address_locality": "New York", - "address_region": "NY", - "address_country": "US", - "postal_code": "10035" - }, - "geo": { - "latitude": 40.790, - "longitude": -73.950 - }, - "amenities": ["in_store_pickup"], - "timezone": "America/New_York", - "hours": [ - { - "day": "monday", - "open": "09:00", - "close": "21:00" - }, - { - "day": "tuesday", - "open": "09:00", - "close": "21:00" - }, - { - "day": "wednesday", - "open": "09:00", - "close": "21:00" - }, - { - "day": "thursday", - "open": "09:00", - "close": "21:00" - }, - { - "day": "friday", - "open": "09:00", - "close": "22:00" - } - ], - "exception_hours": [ - { - "from": "2026-11-26", - "through": "2026-11-27", - "label": "Thanksgiving", - "open": "00:00", - "close": "00:00" - } - ] - } - ] - } - ``` - -#### Example: Partial Success (Some Locations Not Found) +#### Binding envelope example === "Request" @@ -396,7 +135,7 @@ Maps to the [Location Lookup](lookup.md) capability. UCP-Agent: profile="https://platform.example/profiles/v2026-01/agent.json" { - "ids": ["loc_downtown", "loc_invalid_id"] + "ids": ["loc_downtown"] } ``` @@ -419,68 +158,7 @@ Maps to the [Location Lookup](lookup.md) capability. "locations": [ { "id": "loc_downtown", - "name": "Downtown Store", - "address": { - "street_address": "100 Broadway", - "address_locality": "New York", - "address_region": "NY", - "address_country": "US", - "postal_code": "10005" - }, - "geo": { - "latitude": 40.707, - "longitude": -74.011 - }, - "amenities": ["curbside_pickup", "in_store_pickup", "parking"], - "timezone": "America/New_York", - "hours": [ - { - "day": "monday", - "open": "09:00", - "close": "21:00" - }, - { - "day": "tuesday", - "open": "09:00", - "close": "21:00" - }, - { - "day": "wednesday", - "open": "09:00", - "close": "21:00" - }, - { - "day": "thursday", - "open": "09:00", - "close": "21:00" - }, - { - "day": "friday", - "open": "09:00", - "close": "22:00" - }, - { - "day": "saturday", - "open": "10:00", - "close": "20:00" - } - ], - "exception_hours": [ - { - "from": "2026-11-26", - "through": "2026-11-27", - "label": "Thanksgiving", - "open": "00:00", - "close": "00:00" - } - ] - } - ], - "messages": [ - { - "type": "info", - "code": "not_found", - "content": "Unable to find the location associated with loc_invalid_id." + "name": "Downtown Store" } ] } diff --git a/docs/specification/location/search.md b/docs/specification/location/search.md index 3aa4c0e20..04a5f23c2 100644 --- a/docs/specification/location/search.md +++ b/docs/specification/location/search.md @@ -70,12 +70,26 @@ custom filters via `additionalProperties`. ### Hours-Based Filter -Filters locations based on their operating hours: - -* `open_now`: A quick boolean filter to find locations currently open. -* `open_at`: An RFC 3339 date-time string to find locations open at a - specific future time (e.g., planning a visit or ordering ahead). - The business resolves this against the location's local time and timezone. +The standard `filters.hours` object requires `open_at`, an RFC 3339 instant +expressed with `Z` or a numeric offset. + +A Platform selects `open_at` to represent the time relevant to the Buyer's +intent. It can use its current time or choose another time, such as an expected +arrival, pickup, or order-acceptance time. A Platform **MAY** round or adjust +its selected time to the granularity appropriate to the interaction, but the +value it sends identifies one specific instant. + +For each candidate Location, a Business **MUST** evaluate `open_at` exactly as +supplied. It converts the instant to the local date, day of week, and time using +the Location's authoritative `timezone`, then evaluates the effective schedule +under [Operating Hours](index.md#operating-hours). The `Z` or numeric offset in +`open_at` identifies the instant; it does not identify the Location's timezone. + +A Business **MUST** return a Location only when it can establish that the +Location is open at `open_at`. If its schedule data is absent, invalid, outside +the range for which it can evaluate authoritatively, or otherwise unusable, the +Location does not match the filter. A Business **MUST NOT** round, shift, or +otherwise reinterpret the supplied instant. ### Offerings-Based Filter @@ -141,6 +155,144 @@ error. Clients MUST NOT assume the response size equals the requested limit. {{ extension_schema_fields('types/pagination.json#/$defs/response', 'location') }} +## Examples {: #examples } + +The following requests and responses are transport-neutral UCP payloads. + +### Grocery stores serving a point and open at an instant + +=== "Request" + + + ```json + { + "query": "grocery store near me", + "context": { + "address_country": "US", + "address_region": "CA", + "postal_code": "94043" + }, + "filters": { + "hours": { + "open_at": "2026-05-18T17:00:00Z" + }, + "amenities": ["curbside_pickup"], + "geo": { + "serves": { + "point": { + "latitude": 37.422, + "longitude": -122.084 + } + } + } + } + } + ``` + +=== "Response" + + + ```json + { + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.common.location.search": [ + {"version": "{{ ucp_version }}"} + ] + } + }, + "locations": [ + { + "id": "loc_valley_grocers", + "name": "Valley Grocers", + "address": { + "street_address": "789 Maple Ave", + "address_locality": "Mountain View", + "address_region": "CA", + "address_country": "US", + "postal_code": "94043" + }, + "geo": { + "latitude": 37.420, + "longitude": -122.080 + }, + "amenities": ["curbside_pickup", "in_store_pickup", "parking"], + "timezone": "America/Los_Angeles", + "hours": [ + {"day": "monday", "opens": "08:00", "closes": "21:00"} + ] + } + ] + } + ``` + +At the supplied instant, it is Monday at `10:00` in +`America/Los_Angeles`, within the returned interval. See +[Operating Hours](index.md#operating-hours) for complete schedule evaluation +rules. + +### Locations with an inventory item within a distance + +=== "Request" + + + ```json + { + "filters": { + "inventory": [ + { + "id": "item_id_phone_15_pro", + "availability_status": "in_stock" + } + ], + "geo": { + "distance": { + "center": { + "latitude": 40.707, + "longitude": -74.011 + }, + "max_distance": 10000 + } + } + } + } + ``` + +=== "Response" + + + ```json + { + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.common.location.search": [ + {"version": "{{ ucp_version }}"} + ] + } + }, + "locations": [ + { + "id": "loc_downtown_electronics", + "name": "Downtown Electronics", + "address": { + "street_address": "100 Broadway", + "address_locality": "New York", + "address_region": "NY", + "address_country": "US", + "postal_code": "10005" + }, + "geo": { + "latitude": 40.709, + "longitude": -74.008 + }, + "amenities": ["curbside_pickup", "in_store_pickup"] + } + ] + } + ``` + ## Transport Bindings * [REST Binding](rest.md#post-locationssearch): `POST /locations/search` diff --git a/source/schemas/common/types/daily_hour.json b/source/schemas/common/types/daily_hour.json index c95428413..e38035690 100644 --- a/source/schemas/common/types/daily_hour.json +++ b/source/schemas/common/types/daily_hour.json @@ -2,9 +2,9 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://ucp.dev/schemas/common/types/daily_hour.json", "title": "Daily Hour", - "description": "Operating hours for a specific day of the week.", + "description": "A regular weekly operating interval. Its `day`, `opens`, and `closes` are recurring local civil values interpreted in the containing Location's `timezone`. Multiple entries for the same day support split shifts.", "type": "object", - "required": ["day"], + "required": ["day", "opens", "closes"], "allOf": [ { "$ref": "time_interval.json" }, { @@ -12,7 +12,9 @@ "properties": { "day": { "type": "string", - "enum": ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"] + "enum": ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"], + "description": "A stable UCP day-of-week identifier for the day on which this recurring local civil-time interval begins in the containing Location's `timezone`. It is not localized display text.", + "ucp_request": "omit" } } } diff --git a/source/schemas/common/types/exception_hour.json b/source/schemas/common/types/exception_hour.json index 2b8cc6d9e..adffa244e 100644 --- a/source/schemas/common/types/exception_hour.json +++ b/source/schemas/common/types/exception_hour.json @@ -2,27 +2,30 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://ucp.dev/schemas/common/types/exception_hour.json", "title": "Exception Hour", - "description": "Operating hours for a specific date range that differs from normal ones (e.g., holiday or temporary change).", + "description": "A date-specific operating interval or full closure. Its `valid_from`, `valid_through`, `opens`, and `closes` are local civil values interpreted in the containing Location's `timezone`. Date bounds are inclusive.", "type": "object", - "required": ["from", "through"], + "required": ["valid_from", "valid_through"], "allOf": [ { "$ref": "time_interval.json" }, { "type": "object", "properties": { - "from": { + "title": { "type": "string", - "format": "date", - "description": "An ISO 8601 date representing the start of the date range." + "description": "A short human-readable heading naming the exception (for example, 'Thanksgiving'). Presentation metadata that does not affect schedule evaluation.", + "ucp_request": "omit" }, - "through": { + "valid_from": { "type": "string", "format": "date", - "description": "An ISO 8601 date representing the date after the end of the date range." + "description": "The first local civil date to which this exception applies, interpreted in the containing Location's `timezone`.", + "ucp_request": "omit" }, - "label": { + "valid_through": { "type": "string", - "description": "Human readable explanation for the exception (e.g., 'Thanksgiving')." + "format": "date", + "description": "The last local civil date to which this exception applies, interpreted in the containing Location's `timezone`.", + "ucp_request": "omit" } } } diff --git a/source/schemas/common/types/location.json b/source/schemas/common/types/location.json index a8b21ad6a..8f57bf731 100644 --- a/source/schemas/common/types/location.json +++ b/source/schemas/common/types/location.json @@ -25,7 +25,7 @@ }, "hours": { "type": "array", - "description": "Regular weekly operating hours. Support split hours through multiple entries on the same day and overnight hours (open > close). Omission of days implies full day closures.", + "description": "Regular weekly operating hours whose day and time values use this Location's canonical local civil-time frame. Multiple entries for the same day support split shifts. An omitted day has no regular interval beginning that day; an interval beginning on the preceding day can carry into it. Omission of the entire `hours` property means the regular schedule is unknown.", "items": { "$ref": "daily_hour.json" }, @@ -33,7 +33,7 @@ }, "exception_hours": { "type": "array", - "description": "Exception hours for specific dates (holidays, closures, etc.). Support split hours through multiple entries on the same day and overnight hours (open > close). For full day closures, MUST explicitly set open and close to 00:00.", + "description": "Date-specific operating-hour exceptions, including full closures, whose date and time values use this Location's canonical local civil-time frame.", "items": { "$ref": "exception_hour.json" }, @@ -41,11 +41,10 @@ }, "timezone": { "type": "string", - "description": "IANA timezone identifier (e.g., 'America/New_York'). MUST be set when hours or exception_hours are present. Required for correct interpretation.", + "description": "The Business-owned IANA Time Zone Database identifier (e.g., 'America/New_York') defining this Location's canonical local civil-time frame for all returned schedule day, time, and date fields. The Business does not vary this canonical framing by the requesting Platform's or Buyer's timezone. Required when hours or exception_hours is present.", "ucp_request": "omit" } - }, - "additionalProperties": true + } } ], "if": { diff --git a/source/schemas/common/types/location_filter.json b/source/schemas/common/types/location_filter.json index bc31f999e..73791ccbf 100644 --- a/source/schemas/common/types/location_filter.json +++ b/source/schemas/common/types/location_filter.json @@ -50,19 +50,16 @@ }, "hours": { "type": "object", - "description": "Filter by operating hours. If both values are specified, behavior is implementation-defined (usually open_at takes precedence or OR logic is enforced).", + "description": "Filter by operating hours, evaluated at the one supplied instant.", + "required": ["open_at"], "properties": { - "open_now": { - "type": "boolean", - "description": "Only return locations that are currently open." - }, "open_at": { "type": "string", "format": "date-time", - "description": "An RFC 3339 instant. The business converts it to the location's local timezone and evaluates against the known hours." + "pattern": "(?:[Zz]|[+-](?:[01][0-9]|2[0-3]):[0-5][0-9])$", + "description": "The RFC 3339 instant at which matching Locations must be open, expressed with `Z` or a numeric offset. The Platform selects the instant that represents the Buyer's intent. The Business evaluates it exactly as supplied using each Location's authoritative `timezone`; the supplied offset does not identify that timezone." } - }, - "additionalProperties": false + } }, "amenities": { "type": "array", diff --git a/source/schemas/common/types/time_interval.json b/source/schemas/common/types/time_interval.json index c9d2d568b..6fbe365ab 100644 --- a/source/schemas/common/types/time_interval.json +++ b/source/schemas/common/types/time_interval.json @@ -2,19 +2,24 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://ucp.dev/schemas/common/types/time_interval.json", "title": "Time Interval", - "description": "An open time interval with 24-hour HH:MM format.", + "description": "Reusable opening and closing time fields for a containing schedule schema. Containing schemas determine whether the `opens` and `closes` pair is required; this fragment's standalone `{}` is not an interval.", "type": "object", - "required": ["open", "close"], "properties": { - "open": { + "opens": { "type": "string", "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$", - "description": "Start time (e.g., '09:00')." + "description": "Opening time in 24-hour HH:MM format.", + "ucp_request": "omit" }, - "close": { + "closes": { "type": "string", "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$", - "description": "End time (e.g., '18:00')." + "description": "Closing time in 24-hour HH:MM format.", + "ucp_request": "omit" } + }, + "dependentRequired": { + "opens": ["closes"], + "closes": ["opens"] } } From 82c6527f945f4a3e89971fbe31a234485ab9f3fd Mon Sep 17 00:00:00 2001 From: Jing Li Date: Thu, 13 Aug 2026 20:44:43 -0400 Subject: [PATCH 31/37] Cleanup incorrectly placed signature headers in meta object definition. --- source/services/common/mcp.openrpc.json | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/source/services/common/mcp.openrpc.json b/source/services/common/mcp.openrpc.json index 5c7518b1d..9db03a59b 100644 --- a/source/services/common/mcp.openrpc.json +++ b/source/services/common/mcp.openrpc.json @@ -41,18 +41,6 @@ "type": "string", "format": "uuid", "description": "Unique key for retry safety. Maps to HTTP Idempotency-Key header (optional for read-only operations)." - }, - "signature": { - "type": "string", - "description": "RFC 9421 HTTP message signature." - }, - "signature_input": { - "type": "string", - "description": "RFC 9421 Signature-Input header. Format: `sig1=(\"@method\" \"@path\" ...);created=;keyid=\"\"`." - }, - "content-digest": { - "type": "string", - "description": "Body digest per RFC 9530. Format: `sha-256=::`." } } } From 544413f8e5e012fe068a38b4056abde463b38bbc Mon Sep 17 00:00:00 2001 From: Jing Li Date: Thu, 13 Aug 2026 22:03:51 -0400 Subject: [PATCH 32/37] Address feedback on existing contracts consistency. --- docs/specification/location/search.md | 20 +++++++++++++++++-- .../common/types/inventory_filter.json | 2 +- .../schemas/common/types/location_base.json | 13 +++++++++--- .../schemas/common/types/location_filter.json | 6 +----- 4 files changed, 30 insertions(+), 11 deletions(-) diff --git a/docs/specification/location/search.md b/docs/specification/location/search.md index 04a5f23c2..7cc6cc0d4 100644 --- a/docs/specification/location/search.md +++ b/docs/specification/location/search.md @@ -102,14 +102,30 @@ Separates static location characteristics from dynamic availability: the location. Some industry specific use cases include: * *Shopping*: Checking stock availability for specific products or variants. * *Food Ordering*: Checking offering availability of specific dishes or menu items. - Each inventory filter requires an `id` (e.g., product/dish ID) and can optionally specify - a coarse `availability_status` value. + Each inventory filter requires an stable, opaque `id` (e.g., product/dish ID) and + can optionally specify a coarse `availability_status` value. #### Amenity Vocabulary UCP defines an open string vocabulary for amenities via `amenity_type.json` to ensure cross-business interoperability. Implementations **SHOULD** map their internal features to the well-known types where applicable. +#### Inventory Filter Evaluation Rules + +* **Omission Semantics**: When `availability_status` is omitted for an item `id`, + the business **MUST** treat the predicate as requiring that the item is currently orderable/fulfillable + at that location (equivalent to `in_stock`, or active `preorder`/`backorder` with available capacity). +* **Conjunctive Matching**: Multiple entries in `filters.inventory` combine with logical **AND**. + A location matches only if all item predicates are simultaneously satisfied. +* **Contradictory / Impossible Predicates**: If contradictory predicates are supplied + for the same item `id` (e.g., requesting both `in_stock` and `backorder` availability status), the + business **MUST** evaluate the conjunction strictly, returning an empty result set rather than throwing an error. + Business **MAY** include an info message with `code: "contradictory_filters"` to indicate the reason behind the + empty result. +* **Nonexistent Item IDs**: If an item `id` does not exist in the business's domain, that item's + availability predicate is always evaluated as `false`. Business **MUST** return an empty result set and **MAY** append + an info message with `code: "item_not_found"`. + ### Geographic & Geofencing Filter Supports two distinct, industry-agnostic spatial search models: diff --git a/source/schemas/common/types/inventory_filter.json b/source/schemas/common/types/inventory_filter.json index 1166526dd..4c26c9490 100644 --- a/source/schemas/common/types/inventory_filter.json +++ b/source/schemas/common/types/inventory_filter.json @@ -8,7 +8,7 @@ "properties": { "id": { "type": "string", - "description": "The unique identifier of the item (e.g., product or variant ID in shopping, or dish ID in food ordering)." + "description": "The stable, opaque identifier of the item that MUST be Business-resolved (e.g., product or variant ID in shopping, or dish ID in food ordering)." }, "availability_status": { "type": "string", diff --git a/source/schemas/common/types/location_base.json b/source/schemas/common/types/location_base.json index 07975ce24..20ac9a507 100644 --- a/source/schemas/common/types/location_base.json +++ b/source/schemas/common/types/location_base.json @@ -13,8 +13,8 @@ "ucp_request": { "transition": { "from": "omit", - "to": "optional", - "description": "Location ids MAY be specified by platforms in requests." + "to": "required", + "description": "Location ids must be specified by platforms in requests." } } }, @@ -31,7 +31,14 @@ }, "address": { "$ref": "postal_address.json", - "description": "Physical address of the location." + "description": "Physical address of the location.", + "ucp_request": { + "transition": { + "from": "optional", + "to": "omit", + "description": "Location address should not be specified by platforms." + } + } } } } diff --git a/source/schemas/common/types/location_filter.json b/source/schemas/common/types/location_filter.json index 73791ccbf..2d451baa9 100644 --- a/source/schemas/common/types/location_filter.json +++ b/source/schemas/common/types/location_filter.json @@ -39,11 +39,7 @@ "description": "Coarse postal address reference of the target destination (e.g., for ZIP-code based routing)." } }, - "additionalProperties": false, - "oneOf": [ - { "required": ["point"] }, - { "required": ["address"] } - ] + "additionalProperties": true } }, "additionalProperties": false From 8178018c74f173d521dec4f79904c8b3cd11de0b Mon Sep 17 00:00:00 2001 From: Jing Li Date: Thu, 13 Aug 2026 22:38:13 -0400 Subject: [PATCH 33/37] Remodel amenities as reverse-DNS string arrays. --- docs/specification/location/index.md | 4 ++-- docs/specification/location/search.md | 2 +- source/schemas/common/types/amenity_type.json | 12 +++++++----- source/schemas/common/types/location_filter.json | 2 +- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/docs/specification/location/index.md b/docs/specification/location/index.md index 943542f29..f53dfa8b6 100644 --- a/docs/specification/location/index.md +++ b/docs/specification/location/index.md @@ -40,8 +40,8 @@ This is vertical-agnostic and enables key commerce flows such as: address, operating hours, and **geographic context** (geographic coordinates). * **Offerings**: Features, capabilities, and inventory provided by the location. This is split into two distinct concepts to ensure tooling compatibility and semantic clarity: - * **Amenities**: Static features, services, or capabilities of the location - (e.g., `free_wifi`, `parking`, `outdoor_seating`, `curbside_pickup`). + * **Amenities**: Static features, services, or capabilities of the location. Modeled as a flat reverse-DNS array to avoid + semantic ambiguity across diverse industries (e.g., food drive-through vs. pharmacy drive-through). * **Inventory**: Dynamic availability of goods (e.g., retail products or restaurant dishes). * **Geofencing & Service Area**: Locations implicitly carry a geofence boundary around their coordinates. This is used to determine if a location can serve a specific user (e.g., delivery area check). diff --git a/docs/specification/location/search.md b/docs/specification/location/search.md index 7cc6cc0d4..31efb96ea 100644 --- a/docs/specification/location/search.md +++ b/docs/specification/location/search.md @@ -107,7 +107,7 @@ Separates static location characteristics from dynamic availability: #### Amenity Vocabulary -UCP defines an open string vocabulary for amenities via `amenity_type.json` to ensure cross-business +UCP defines an open reverse-DNS vocabulary for amenities via `amenity_type.json` to ensure cross-business interoperability. Implementations **SHOULD** map their internal features to the well-known types where applicable. #### Inventory Filter Evaluation Rules diff --git a/source/schemas/common/types/amenity_type.json b/source/schemas/common/types/amenity_type.json index ca0f3838e..1a658a8df 100644 --- a/source/schemas/common/types/amenity_type.json +++ b/source/schemas/common/types/amenity_type.json @@ -2,11 +2,13 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://ucp.dev/schemas/common/types/amenity_type.json", "title": "Amenity Type", - "description": "The type of amenity supported by a vertical-agnostic location. Some well-known values are defined below as examples and have standardized semantics; freeform codes are permitted.", - "type": "string", + "description": "A standardized open reverse-DNS string representing a physical feature, capability, or service provided by a location. Supports a 2-tier hierarchy when defining well-known values in UCP: 1) Common vocabulary for universal features that directly live on `dev.ucp.amenity` (e.g., dev.ucp.amenity.wi_fi) and 2) Industry-scoped features that MUST append their related service name on top of `dev.ucp.amenity` (e.g., dev.ucp.amenity.shopping.in_store_pickup). Businesses MAY define custom vocabulary in their own domain (e.g., com.example.amenity.auto_care_center).", + "$ref": "reverse_domain_name.json", "examples": [ - "in_store_pickup", - "curbside_pickup", - "drive_through" + "dev.ucp.amenity.wi_fi", + "dev.ucp.amenity.parking", + "dev.ucp.amenity.shopping.in_store_pickup", + "dev.ucp.amenity.shopping.curbside_pickup", + "dev.ucp.amenity.shopping.drive_through" ] } diff --git a/source/schemas/common/types/location_filter.json b/source/schemas/common/types/location_filter.json index 2d451baa9..9e99138a0 100644 --- a/source/schemas/common/types/location_filter.json +++ b/source/schemas/common/types/location_filter.json @@ -62,7 +62,7 @@ "items": { "$ref": "amenity_type.json" }, - "description": "Filter by static services, amenities, or capabilities of the location (e.g., 'free_wifi', 'wheelchair_accessible', 'parking', 'curbside_pickup'). Matches locations that have ALL the listed amenities (AND logic)." + "description": "Filter by static services, amenities, or capabilities of the location. Matches locations that have all the listed amenities (AND logic)." }, "inventory": { "type": "array", From c1a1396b62c74ee7b113d84b0e620a68b343fbee Mon Sep 17 00:00:00 2001 From: Jing Li Date: Thu, 13 Aug 2026 22:49:33 -0400 Subject: [PATCH 34/37] Fix documentation examples. --- docs/specification/location/lookup.md | 6 +++++- docs/specification/location/search.md | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/specification/location/lookup.md b/docs/specification/location/lookup.md index de4bd050f..d33766d5a 100644 --- a/docs/specification/location/lookup.md +++ b/docs/specification/location/lookup.md @@ -115,7 +115,11 @@ The following request and response are transport-neutral UCP payloads. "latitude": 40.707, "longitude": -74.011 }, - "amenities": ["curbside_pickup", "in_store_pickup", "parking"], + "amenities": [ + "dev.ucp.amenity.shopping.curbside_pickup", + "dev.ucp.amenity.shopping.in_store_pickup", + "dev.ucp.amenity.parking" + ], "timezone": "America/New_York", "hours": [ {"day": "monday", "opens": "09:00", "closes": "21:00"}, diff --git a/docs/specification/location/search.md b/docs/specification/location/search.md index 31efb96ea..e47d4e9d0 100644 --- a/docs/specification/location/search.md +++ b/docs/specification/location/search.md @@ -192,7 +192,7 @@ The following requests and responses are transport-neutral UCP payloads. "hours": { "open_at": "2026-05-18T17:00:00Z" }, - "amenities": ["curbside_pickup"], + "amenities": ["dev.ucp.amenity.shopping.curbside_pickup"], "geo": { "serves": { "point": { @@ -233,7 +233,7 @@ The following requests and responses are transport-neutral UCP payloads. "latitude": 37.420, "longitude": -122.080 }, - "amenities": ["curbside_pickup", "in_store_pickup", "parking"], + "amenities": ["dev.ucp.amenity.shopping.curbside_pickup", "dev.ucp.amenity.shopping.in_store_pickup", "dev.ucp.amenity.parking"], "timezone": "America/Los_Angeles", "hours": [ {"day": "monday", "opens": "08:00", "closes": "21:00"} @@ -303,7 +303,7 @@ rules. "latitude": 40.709, "longitude": -74.008 }, - "amenities": ["curbside_pickup", "in_store_pickup"] + "amenities": ["dev.ucp.amenity.shopping.curbside_pickup", "dev.ucp.amenity.shopping.in_store_pickup"] } ] } From 3ebd1b2d39c0b5db053f615bc225b4afc392b076 Mon Sep 17 00:00:00 2001 From: Jing Li Date: Thu, 13 Aug 2026 23:03:25 -0400 Subject: [PATCH 35/37] Fix serves(target) contextual fallback algorithm. --- docs/specification/location/search.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/specification/location/search.md b/docs/specification/location/search.md index e47d4e9d0..b5a8bd44b 100644 --- a/docs/specification/location/search.md +++ b/docs/specification/location/search.md @@ -146,9 +146,9 @@ Supports two distinct, industry-agnostic spatial search models: > **Contextual Fallback**: If the `serves` filter is not explicitly specified in > the request, the business **MAY** use the user's contextual location hints passed in the > request `context` object to implicitly apply a `serves` filter, returning only locations -> that can service the user. Similarly to the callout above, if `context` is omitted or -> business is unable to resolve the provided address hint, then business **MUST** return -> an error message with `code: "location_geo_filter_resolution_failed"`. +> that can service the user. If `context` is omitted or business is unable to resolve the +> provided address hint, unlike the treatment above for `distance`, business **SHOULD** +> interpret this unqualified filter predicate as "serves any location target". ## Pagination From f687a657ec070118a32ebcaf7c7af2643fc35fab Mon Sep 17 00:00:00 2001 From: Jing Li Date: Thu, 13 Aug 2026 23:19:05 -0400 Subject: [PATCH 36/37] Revert back the changes to retail_destination.json to decouple the scope. --- .../types/fulfillment_destination.json | 3 +-- .../shopping/types/retail_location.json | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 source/schemas/shopping/types/retail_location.json diff --git a/source/schemas/shopping/types/fulfillment_destination.json b/source/schemas/shopping/types/fulfillment_destination.json index d9af73dd0..673b4bdbd 100644 --- a/source/schemas/shopping/types/fulfillment_destination.json +++ b/source/schemas/shopping/types/fulfillment_destination.json @@ -9,8 +9,7 @@ "$ref": "shipping_destination.json" }, { - "$ref": "../../common/types/location_base.json", - "description": "A pickup retail location (e.g. store, locker, etc.)." + "$ref": "retail_location.json" } ] } diff --git a/source/schemas/shopping/types/retail_location.json b/source/schemas/shopping/types/retail_location.json new file mode 100644 index 000000000..2e9d5d88f --- /dev/null +++ b/source/schemas/shopping/types/retail_location.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/retail_location.json", + "title": "Retail Location", + "description": "A pickup location (retail store, locker, etc.).", + "type": "object", + "ucp_shared_request": true, + "required": ["id", "name"], + "additionalProperties": true, + "properties": { + "id": { + "type": "string", + "description": "Unique location identifier.", + "ucp_request": "omit" + }, + "name": { + "type": "string", + "description": "Location name (e.g., store name)." + }, + "address": { + "$ref": "../../common/types/postal_address.json", + "description": "Physical address of the location." + } + } +} From 1fdd7910ff1fef738c55001eaaf8b186c5c08c28 Mon Sep 17 00:00:00 2001 From: Jing Li Date: Thu, 13 Aug 2026 23:22:11 -0400 Subject: [PATCH 37/37] Remove transition annotation from location_base.json as this is now being treated as a net new schema type. --- .../schemas/common/types/location_base.json | 24 +++---------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/source/schemas/common/types/location_base.json b/source/schemas/common/types/location_base.json index 20ac9a507..8717a0771 100644 --- a/source/schemas/common/types/location_base.json +++ b/source/schemas/common/types/location_base.json @@ -10,35 +10,17 @@ "id": { "type": "string", "description": "Unique location identifier.", - "ucp_request": { - "transition": { - "from": "omit", - "to": "required", - "description": "Location ids must be specified by platforms in requests." - } - } + "ucp_request": "required" }, "name": { "type": "string", "description": "Location display name.", - "ucp_request": { - "transition": { - "from": "required", - "to": "omit", - "description": "Location names should not be specified by platforms." - } - } + "ucp_request": "omit" }, "address": { "$ref": "postal_address.json", "description": "Physical address of the location.", - "ucp_request": { - "transition": { - "from": "optional", - "to": "omit", - "description": "Location address should not be specified by platforms." - } - } + "ucp_request": "omit" } } }