feat!: Introduce Location Search + Lookup capabilities - #589
Conversation
…ing/ to generally represent any physical location (to be referenced by Location capability).
There was a problem hiding this comment.
Overall, the shape and proposal make sense—ty for scaffolding this. A few design questions I'd like to work through...
1. Should common be the service, or the namespace containing a Location service?
The capability names are scoped to Location:
dev.ucp.common.location.search
dev.ucp.common.location.lookup
The profile examples, however, advertise the transport as:
{
"services": {
"dev.ucp.common": [{ "...": "..." }]
}
}I agree with dev.ucp.common.* as the namespace for cross-vertical primitives. However, I'm not sure about making dev.ucp.common one catch-all service: every future shared capability would then share an endpoint, transport schema, etc.
The alternative split is:
namespace: dev.ucp.common.*
service: dev.ucp.common.location
capabilities: dev.ucp.common.location.search
dev.ucp.common.location.lookup
Do we think a catch-all .common service is preferred to scoped services?
2. Is geofence_radius sufficient to model the domain?
The current geo object combines a physical point with a circular geofence_radius, while geofence_point asks whether a requested point falls inside that circle. This covers a useful simple case, but in my experience service areas are often irregular polygons, disjoint regions, postal-code unions, etc.
Do we need to model (thin, hopefully) RFC 7946-based service-area shape supporting Polygon / MultiPolygon?
3. Can we adopt a Schema.org-inspired shape instead of inventing a bespoke one?
The latest changes make the custom model deterministic by defining 24:00, precedence between is_closed / is_24_hours / intervals, and a two-entry convention for overnight hours. That is an improvement, but it also highlights how much new protocol syntax and interpretation logic we are defining.
Schema.org's OpeningHoursSpecification already provides the core semantics we need:
dayOfWeek,opens, andcloses;- multiple entries for split shifts;
closes < opensmeans the interval spans the next day;- no
opensmeans closed; validFrom/validThroughcover date-specific exceptions.
A Schema.org-inspired UCP shape could look like:
{
"timezone": "America/New_York",
"hours": [
{ "day_of_week": "monday", "opens": "09:00", "closes": "17:00" },
{ "day_of_week": "monday", "opens": "18:00", "closes": "22:00" },
{ "day_of_week": "friday", "opens": "22:00", "closes": "02:00" }
],
"exception_hours": [
{
"valid_from": "2026-12-24",
"valid_through": "2026-12-24",
"opens": "09:00",
"closes": "14:00"
},
{
"valid_from": "2026-12-25",
"valid_through": "2026-12-25"
}
]
}The contract is compact: each entry describes one open interval; repeated entries for the same day represent split shifts; a closes value earlier than opens spans midnight; and a date-bounded exception without opens means closed for that period. timezone supplies the interpretation context for local times. The example adapts Schema.org's semantics to UCP's snake_case naming and HH:MM convention rather than copying its JSON-LD representation verbatim.
Is there a concrete requirement that OpeningHoursSpecification cannot model? If not, I would align to it and remove the custom closed/24-hour flags, precedence rules, and overnight splitting convention.
4. Is offerings.inventory.quantity an inventory-disclosure query?
The store-finder use case—"which nearby location can fulfill the item I need?"—is valuable. A hard minimum quantity filter, however, lets a caller repeatedly probe thresholds and approximate a store's stock level even when the response never returns a count.
I suggest framing this as Buyer demand and coarse availability, not inventory disclosure:
- reuse UCP's existing availability semantics rather than parallel inventory concept;
- treat requested quantity as intent that a Business may coarsen;
- change language to not imply that exact on-hand counts are returned;
5. What does offerings mean? 😅
Modelled request can filter on:
{
"offerings": {
"amenities": ["..."],
"inventory": [{ "id": "..." }]
}
}The standardized Location entity, however, exposes neither amenities nor offerings. Although the filter contract implies that each returned Location satisfies the requested values, a Platform cannot render the matched facts, inspect additional amenities, or present structured item availability from the response without relying on vendor-defined fields. There is also a category question: amenities are relatively static Location characteristics, while item availability is dynamic and tied to another Catalog or menu identity. Grouping both under offerings may be convenient structurally, but does not necessarily make them one semantic concept.
My inclination would be to put static amenities on Location, model dynamic availability separately using the existing UCP availability vocabulary, and flatten the filter unless offerings gains a clearer cross-vertical contract.
6. What amenity vocabulary is interoperable across Businesses and verticals?
An open string is the right wire type for UCP, but the current proposal defines no well-known values. Should UCP publish a small open vocabulary for interoperable matching, and which initial values are important enough to standardize?
7. What are the capability-specific security and privacy requirements?
Location handles Buyer location hints, enumerates physical sites, describes service coverage, and can be used to probe per-location item availability. The generic Signals reference and current privacy note do not cover those capability-specific risks.
I think the specification needs explicit Security and Privacy Considerations covering:
- coarse-by-default Buyer location and progressive disclosure;
- purpose limitation and retention of location inputs;
- store/site enumeration and rate limiting;
- inventory and availability probing;
- disclosure of internal, private, or non-buyer-visible locations;
- disclosure of precise service-area geometry.
8. What is the bounded Location projection for Catalog and Checkout?
This PR points Checkout’s pickup destination directly at the rich common Location entity. That couples Checkout to the complete Location discovery model: geo, hours, service area, amenities, and every future Location extension would automatically accrete into the transactional response. I don't think that's right.
In Catalog Fulfillment (#507), we deliberately introduced a thin Location projection: a stable location ID plus method description. This avoided embedding an N-store matrix in product results and deferred richer Location facts to a separately negotiated capability. That boundary still makes sense, but I think there is a middle ground: split Location into a bounded base and an extended discovery entity.
Base Location
id
name
address?
│
└── allOf → Extended Location
geo
hours
exception_hours
timezone
amenities
service_area
Catalog and Checkout would use the bounded, buyer-renderable base. Location Search/Lookup would return the extended entity when the service is negotiated. Platform requests would select a Location by stable id rather than asserting Business-owned name/address facts. This preserves the bounded Catalog model from #507, keeps Catalog and Checkout independently renderable, and prevents the full Location discovery model from accreting into every Checkout response.
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.
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.
|
+1 on scoping the service to The setup. Suppose "services": {
"dev.ucp.common": [{
"version": "2026-04-08",
"transport": "rest",
"schema": ".../services/common/rest.openapi.json",
"endpoint": "https://business.example.com/ucp"
}]
}One endpoint, one transport document, and one version covering store lookup, review retrieval, and identity linking. That seems to make three fairly ordinary deployments unexpressible:
The scoped form expresses all three directly, including the case where two domains happen to share a host: "services": {
"dev.ucp.common.location": [{
"version": "2026-04-08", "transport": "rest",
"schema": ".../services/common/location/rest.openapi.json",
"endpoint": "https://stores.business.example.com/ucp"
}],
"dev.ucp.common.reviews": [{
"version": "2026-01-11", "transport": "rest",
"schema": ".../services/common/reviews/rest.openapi.json",
"endpoint": "https://ucp.reviews-vendor.example.com/business-123"
}],
"dev.ucp.common.identity": [{
"version": "2026-04-08", "transport": "rest",
"schema": ".../services/common/identity/rest.openapi.json",
"endpoint": "https://business.example.com/ucp"
}]
}The asymmetry I think settles it: scoped services can emulate the catch-all, but the catch-all cannot emulate scoped. A business serving everything from one gateway just points every scoped entry at the same Would it be worth writing the rule down explicitly, something like: a service is the unit of deployment, not the unit of naming? A new service is warranted whenever a domain could plausibly be operated by a different party, on different infrastructure, or on a different release cadence than its siblings. I like that framing because it explains the existing corpus rather than contradicting it. |
For suggestion 2. above could we just drop the circle instead of upgrading it to polygons?
That matters because publishing it as a circle is currently self-contradictory. Proposal: drop
Why this looks better than adding |
…sed ucp annotation for fulfillment related files.
|
Thanks @igrigorik and @amithanda for the extensive feedback! A lot of great points that further helped to tune the schema shape. 1. Should
|
| "components": { | ||
| "parameters": { | ||
| "authorization": { | ||
| "name": "Authorization", |
There was a problem hiding this comment.
Authorization cannot be modeled as an ordinary header parameter in OpenAPI 3.1; compliant tooling is required to ignore a header parameter with that name. Because this document also has no securitySchemes or root/operation security requirements, generated clients and gateways will interpret these operations as unauthenticated. UCP-Agent identifies a claimed profile but does not prove who sent the request. Please model the supported authentication alternatives through securitySchemes and operation-level security, explicitly represent anonymous public search if that is supported, and state that authentication and authorization occur before location-ID resolution or filtering.
There was a problem hiding this comment.
That's a great find and I think one that is also affecting our existing openapi services - e.g., https://github.com/Universal-Commerce-Protocol/ucp/blob/main/source/services/shopping/rest.openapi.json#L808! To keep the scope of the PR focused on the new capability, I'm going to keep this as-is here but have filed #668 to track a quick follow-up fix that would universally apply to all openapi service definitions we have.
| } | ||
| }, | ||
| "responses": { | ||
| "200": { |
There was a problem hiding this comment.
Both operations currently document only a 200 response even though the specification defines invalid-request, authentication, rate-limit, and server-error behavior. Autonomous clients need machine-readable distinctions between terminal errors, caller-correctable errors, and retryable failures; otherwise they may retry unauthorized requests, silently abandon valid searches, or amplify an outage. Please add the shared UCP error envelope for at least 400, 401, 403, 429, and 5xx, require Retry-After where appropriate, and align the REST and MCP behavior for an oversized lookup batch. The current documentation alternates between implementation-defined limits and a 400; please choose one normative status and error code.
There was a problem hiding this comment.
Another great callout as I also recall seeing feedback from the community - #131 (section in Standardize Error Responses on explicitly mapping out protocol-level errors.
I'd like to keep this PR focused on the capability design and propose a follow-up to enhance all openapi definitions (e.g., https://github.com/Universal-Commerce-Protocol/ucp/blob/main/source/services/shopping/rest.openapi.json).
| "format": "uuid", | ||
| "description": "Unique key for retry safety. Maps to HTTP Idempotency-Key header (optional for read-only operations)." | ||
| }, | ||
| "signature": { |
There was a problem hiding this comment.
This introduces a detached JWS in the MCP JSON metadata, while UCP specifies RFC 9421 HTTP Message Signatures for HTTP transports, including MCP Streamable HTTP. These mechanisms are not wire-compatible and the new field leaves canonicalization, covered fields, key resolution, and replay parameters underspecified. Please remove the JSON-level detached JWS and use Signature-Input, Signature, and Content-Digest at the HTTP transport layer, consistent with the other UCP services. If a non-HTTP MCP transport is introduced later, its signing model should be specified independently rather than overloading this metadata field.
There was a problem hiding this comment.
17dac60 corrected the terminology but not the protocol layer: signature, signature_input, and content-digest remain JSON members of meta. RFC 9421 defines Signature and Signature-Input as HTTP fields, and UCP's existing MCP signing guidance places those fields plus Content-Digest in HTTP headers and explicitly avoids a duplicate JSON representation. Implementations following this OpenRPC document would sign a different wire contract than implementations following the core signing specification.
…e more flexible combination of request inputs.
…how business should handle contextual hints fallback.
|
Thanks @maximenajim for the feedback! Responses added and requesting for another passthrough. |
| }, | ||
| { | ||
| "$ref": "retail_location.json" | ||
| "$ref": "../../common/types/location_base.json", |
There was a problem hiding this comment.
Every location_base instance has id and name, while shipping_destination requires only id and permits unknown properties through its composed postal-address schema. A normal BOPIS destination such as {"id":"store-1","name":"Downtown","address":{"postal_code":"55403"}} therefore matches both branches and is rejected by oneOf; the current schema makes the bounded pickup projection unusable in Checkout.
Please select the destination schema from the containing fulfillment method—for example, type: "shipping" constrains destinations.items to shipping_destination and type: "pickup" constrains them to location_base—or add an explicit discriminator that makes the branches mutually exclusive. Add a conformance case with id, name, and address.
| }, | ||
| "ucp_request": "omit" | ||
| }, | ||
| "hours": { |
There was a problem hiding this comment.
The revised interval behavior is inspired by schema.org, but the wire representation still replaces its deployed vocabulary with hours, exception_hours, day, open, close, from, and through. Existing merchant JSON-LD uses openingHoursSpecification, specialOpeningHoursSpecification, dayOfWeek, opens, closes, validFrom, and validThrough; schema.org-aware generators and validators will not recognize the proposed fields, and through also uses a different exclusive-date convention.
|
I completed another pass. The proposal is looking good. I added a few more follow-up comments. |
Description
A mirror copy of #545 that supersedes it.
Defines standard interfaces for discovering, searching, and retrieving physical locations (e.g., retail stores, restaurants, warehouses, lodging properties).
It introduces two new capabilities under the
dev.ucp.commonnamespace (consistent with Shopping's Catalog capability design):dev.ucp.common.location.search): Discovery-focused endpoint for natural language query, geographic, and offerings-based filters.dev.ucp.common.location.lookup): Resolution-focused endpoints supporting single & batch lookups.Some key commerce flows it will be able to unlock:
nearby that support customer pickup and checking their operating hours & inventory availability
before selection.
or local service provider) has delivery coverage for a buyer's address.
Category (Required)
ucp-schematool (resolver, linter, validator). (Requires Maintainer approval)Related Issues
This is related to RFC #375's section 10.
Checklist
!for breaking changes).Screenshots / Logs (if applicable)