search_restaurants
Return live restaurants the calling agent can discover and order from (every approved agent sees the same set), relevance-ranked when aquery is present.
Input:
latitude/longitude— true proximity. No implicit radius; passradius_miles(max 50) to bound it. Preferred whenever the client has geometry (device location, address autocomplete). Stores with no stored coordinates come back ranked last withdistanceMiles: null— but only while the search is unbounded. Addingradius_milesis a promise (“within N miles”) that cannot be checked against an unknown location, so it drops them.postal_code(ZIP or ZIP+4) — resolved to the ZIP’s geographic centroid, then the same distance ranking as coordinates with a default 25-mile radius (override withradius_miles). An unknown ZIP (PO-box-only, military) degrades to exact-ZIP string matching. Stores without stored coordinates that exactly match the searched ZIP are still included, distance-unranked.city/state— hard textual filters (state accepts “CA” or “California”); they also combine with any of the above.address— never geocoded. A ZIP found inside it gets centroid treatment. With no ZIP and no coordinates, the address is parsed into city/state and runs the same hard filters as (3): the last comma-separated fragment is read as the state when it looks like one, the fragment before it as the city, and a bare street line (“1 Main St”) yields no locality. Explicitcity/statealways win over the parsed ones, and when an origin resolved the parsed locality is ignored — distance already scopes the search.
latitudewithoutlongitude(or the reverse) throws. A single coordinate resolves no origin, so the search used to fall through tono_results_reason: "location_required"— telling you to send a location you had just sent.radius_mileswith no origin throws. There is nothing to measure from on acity/statescope, so the radius was ignored and a store several states away came back forradius_miles: 1. Pairradius_mileswithlatitude/longitude, or with apostal_code(or an address whose ZIP resolves to a centroid).
- A location is required for restaurant results. A
querywith no location returnsbrand_matchessummaries instead (see below) — never a list of arbitrary far-away stores. querymatches name, brand, cuisine, description, and address with normalization (case, accents, apostrophes, ”&”/“and”) and any-order token matching (“leaf salads green” finds “Green Leaf Salads”). Each result carriesmatch_score(0-100) andmatched_on(name | brand | cuisine | description | address).- Default ordering: relevance when
queryis present (distance, then name, as tie-breaks); distance when an origin resolved; name otherwise.sortoverrides.sort: "prep_time"treats 0/unpublished prep as unknown (sorted last). - Pagination —
offsetskips ranked results; the envelope always carriestotal_matches(pre-pagination count) andnext_offsetwhen more pages exist. An offset past the end returns an empty page without ano_results_reason— reasons describe the match set, not the page. - Projection —
fieldsreturns only the named fields per restaurant (idis always included, andmatch_score/matched_onalways survive when a query ran; unknown names are ignored). Use it for cheap disambiguation calls before pulling full entries. collapse_brands: truereturns only the best-ranked location per brand (nearest under distance sort, most relevant under relevance sort), annotated withbrand_location_count. Independents always pass.limitdefaults to 20, max 50.
requested_time (ISO, offset accepted) annotates every result at that instant; it never filters on its own:
hours_known— false when the store has no stored hours (an empty hours list means “no known closed hours”, so open flags stay optimistically true; qualify accordingly).open_at_requested_time— store hours at the requested instant, in the restaurant’s own timezone.next_open_at— when closed: the ISO instant the store next opens (the probe walks ~10 days ahead); null when open or hours unknown.orderable_at_requested_time— open AND inside both ends of validation’s time gate: far enough out to clear the exact lead-time floor (prep + busy delay, courier floor for delivery whenfulfillment_type: "delivery"is set, notice) yet within the scheduling horizon, AND not blocked by a paused kitchen (paused blocks only orders due within 120 minutes — the same window order validation enforces).only_open: truehard-filters to open stores atrequested_time— or at the current time when sent alone.open_now: trueis the same filter evaluated at the current time (its orderable flag skips the lead floor — ASAP orders resolve their own time). Combiningopen_nowwithrequested_timeis contradictory and rejected with an error. The envelope’sevaluated_timeechoes the instant used.earliest_orderable_time(present on every full entry, may be null; like any other field it must be named infieldsto survive a projection) — the soonest ISO instant an order could be scheduled for: now + the exact server lead floor (prep + busy delay, courier floor whenfulfillment_type: "delivery"is set, notice), pushed past a paused kitchen’s 120-minute block, then to the next opening when that instant lands in closed hours. Null when the store has hours but no opening in the next ~10 days, or when the resulting instant lies beyond the platform scheduling horizon. Computed against the same “now” for every result in a response.
cuisine (normalized substring), min_rating (unrated stores fail it — a quality bar), max_prep_time_minutes (unpublished prep passes), supports_catering (literal boolean match), max_minimum_order_cents (no known minimum passes), exclude_paused.
Output envelope:
agentis the calling agent’s own identity (and its partner), on the envelope — once per response, not repeated on every restaurant. It is present even when the result set is empty, including when the agent can see no restaurants at all. It moved here on 2026-08-11: it is identical on every row, and atlimit: 50the per-entry copies cost roughly 3.5k tokens of the same object. It is not returned insiderestaurants[]any more, andfieldscannot reintroduce it. (The internal REST listing still carries it per entry; only the MCP tool changed.)originis present whenever a search origin resolved;sourcesays how.no_results_reasonis present only whenrestaurantsis empty, so agents can recover instead of guessing:location_required— ask for a location;no_restaurants_in_area— the area has no stores at all;no_match_in_area— the area has stores but query/fulfillment/declarative filters removed everything (widen the query);all_closed_at_requested_time— matches existed butonly_open/open_nowremoved them all (offer a later time; each removed store’snext_open_atwould have said when).evaluated_timeis present whenever availability was evaluated (requested_timeoropen_now).brand_matches(only onlocation_requiredwith aquery) answers “do you carry X anywhere”: brand-level summaries with cities, deliberately without restaurant objects — without an origin any location list is arbitrary, and the agent should ask “which area?” before ordering. Independents are summarized the same way (brand_id: null,location_count: 1). Cities cap at 10 (cities_truncated), matches at 10.
earliest_orderable_time; when query was given: match_score and matched_on; when availability was evaluated: hours_known, open_at_requested_time, next_open_at, orderable_at_requested_time. Snake_case and camelCase duplicates (image_url/imageUrl, cuisine_type/cuisine) are deliberate back-compat aliases.
A restaurant is omitted entirely when agent ordering is off, the restaurant is not payout-ready, or its POS channel has been deactivated (re-activation restores it automatically).
deliverThatCoverage is a cached advisory probe — the checkout quote (or check_delivery_availability) is authoritative.
On marketplace supply (capabilities.supply: "marketplace"), only query, the location, limit/offset and these apply:
price_tiers— price tiers to include,1(cheapest) to4. The provider filters.max_delivery_eta_minutes— prefer stores whose delivery estimate is at or under this many minutes. The provider filters.fulfillment_type: "pickup"— drops stores the provider says don’t offer pickup.
pickup: { "offered": true, "asap": "available_now", "scheduled": "available_now", "nextAsapOpenAt": null }. asap/scheduled are available_now, available_later, unavailable or unknown; offered: null means the provider sent no pickup signal. If a filtered search comes back empty, relax one filter before concluding nothing matches.
search_menu_items
Cross-restaurant dish search: “who has pad thai near me”, “vegan options in 94063”. Finds menu items matching a query across every restaurant in a location scope, relevance-ranked, each with its restaurant attached. Input:queryis required. Location follows the same contract assearch_restaurants, including the rejectedlatitude-without-longitudeand origin-lessradius_milesinputs. Alternativelyrestaurant_idstands in for a location (“does this store have X”) and ignores the location fields entirely — including those two guards.fulfillment_typeis honored on both paths, and an empty result caused by it says so: whether you named one store or a whole area, if the scope holds restaurants but none of them offer that fulfillment type you getno_results_reason: "fulfillment_type_not_supported", never a misleadingno_restaurants_in_area. The recovery is “try pickup”, not “try a different neighborhood”.- Rate limit: 60 calls/min per agent, separate from and stricter than the per-agent MCP quota — each call can scan up to 25 menus. Exceeding it throws a tool error naming the limit; retry after the window resets.
- Matching: item name gets the full normalized/token ladder; dietary/allergen tags (“Vegan”, “Gluten Free”), category, and description are substring-only at reduced weight. Each item carries
match_scoreandmatched_on(name | tags | category | description). - Only currently-available items are returned.
requested_timeadditionally annotatesavailable_at_requested_timefrom the item’s own availability windows (menu/category hours) in the restaurant’s timezone, and putsopen_at_requested_timeon the item’srestaurantblock — an item can be inside its own windows while the store itself is closed; check both. per_restaurant_limit(default 3, max 20) keeps one giant menu from flooding results;limitcaps the total (default 20, max 50);max_price_centsfilters on the item price.- Scans up to 25 stores in scope — nearest first when a coordinate or postal origin resolved, otherwise alphabetical (a city/state-only scope has no distances to rank by, so send coordinates or a
postal_codewhen coverage matters).restaurants_scanned/restaurants_truncatedreport the actual coverage. A store whose menu fails to load is skipped, never fatal. - Sort:
match_scoredesc, then restaurant distance, then price, then name.
search_restaurants (offset + total_matches/next_offset). Note total_matches counts returnable items — matches surviving per_restaurant_limit — not every matching item on every menu.
Follow up with get_menu on the chosen restaurant for modifier groups before building an order — item results carry no modifier data.
get_availability_calendar
The concrete time windows an agent may schedule an order inside, per restaurant-local day: opening hours intersected with the earliest orderable instant (lead floor + busy state) and the platform scheduling horizon. Continuous windows, not slots — any instant inside them passes time validation (per-item availability may still apply). Replaces guess-and-checkvalidate_order loops when picking a fulfillment time.
Input:
days defaults to the platform’s scheduling horizon and is capped by it (default 7 days). Layouts never exceed 14 days — days’ own maximum — whatever the horizon says. fulfillment_type picks which lead floor applies (delivery adds the courier floor); defaults to pickup. The calendar refreshes opening hours from the POS on the same path as get_restaurant_detail, so both surfaces see equally fresh hours.
Output:
- The first window opens at
earliest_orderable_time, never earlier, and the last closes no later thanhorizon_end. hours_known: false(no stored hours) means the store is treated as always open, laid out as one window per restaurant-local day — today from the earliest orderable instant, then each following day midnight to midnight in the restaurant’s own timezone, ending at the horizon. Consecutive windows meet exactly, so the orderable set is continuous;daysis a real per-day layout you can offer a user, not a single entry spanning the whole horizon.- Empty
dayswith hours known means no opening inside the horizon. - Overnight service windows (e.g. Fri 22:00–02:00) appear in full under the local date they open on. A service that opened yesterday and is still running is included too, clipped to the earliest orderable instant — so the calendar never disagrees with
open_at_requested_timeabout a store that is open right now. - Windows are merged per day: hours contributed by several menus (a lunch menu inside an all-day one) arrive as one continuous window, never as overlapping fragments.
list_brand_locations
Expand a chain: every location of a brand this agent can order from. The follow-up tocollapse_brands (“3 other locations”) and to brand_matches summaries from a location-less name query.
Input — one of:
brand_id is exact; brand_name is relevance-matched (best 5 brands, independents included as single-location “brands” with brand_id: null).
Output:
{ "brands": [] }. Location entries are deliberately trimmed — follow up with get_restaurant_detail for hours, timing, and busy state.
list_saved_addresses
Saved delivery addresses on the supply provider’s own account. Only meaningful whencapabilities.addresses is provider_saved — such providers can only deliver to these addresses, so delivery dropoffs must be chosen from this list. On free_form supply the tool errors; fall back to normal address entry.
Input: {}
Output:
find_address_candidates
Resolve a free-text street address into concrete candidates for the consumer to confirm. Read-only first step of adding a new delivery address onprovider_saved supply. Check list_saved_addresses first — if the address is already saved, pass its id to save_delivery_address instead of adding a duplicate.
Input: { "query": "303 2nd St, San Francisco" }
Output:
save_delivery_address with its placeId.
save_delivery_address
Account-wide write onprovider_saved supply: sets the provider account’s default delivery address — the dropoff every subsequent delivery order uses, shared across every consumer of the account. Requires orders:submit scope and confirmed: true, which may only be sent after the consumer explicitly confirmed the exact address.
Pass exactly one of:
place_id— afind_address_candidatescandidate the consumer confirmed. Saves a new address (no dedupe — checklist_saved_addressesfirst). Optional:description,address_type(house, apartment, hotel, office, other),subpremise,delivery_instructions.address_id— an existing entry fromlist_saved_addressesto select as default.
{ "confirmed": true, "place_id": "dXJuOm1ieGFkcjoyZjM4Y2I5Ny0…", "subpremise": "Suite 200" }
Output: the refreshed list_saved_addresses payload, with the new or selected entry carrying isDefault: true.