Skip to main content
Four tools answer “where can I order, what do they serve, and when?” — relevance-ranked search, cross-restaurant dish search, schedulable time windows, and chain expansion.

search_restaurants

Return live restaurants the calling agent can discover and order from (every approved agent sees the same set), relevance-ranked when a query is present. Input:
Location resolution, in priority order:
  1. latitude/longitude — true proximity. No implicit radius; pass radius_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 with distanceMiles: null — but only while the search is unbounded. Adding radius_miles is a promise (“within N miles”) that cannot be checked against an unknown location, so it drops them.
  2. 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 with radius_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.
  3. city / state — hard textual filters (state accepts “CA” or “California”); they also combine with any of the above.
  4. 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. Explicit city/state always win over the parsed ones, and when an origin resolved the parsed locality is ignored — distance already scopes the search.
Two location inputs are rejected rather than quietly dropped, because honoring them is impossible and ignoring them returns a confident answer to a question you did not ask:
  • latitude without longitude (or the reverse) throws. A single coordinate resolves no origin, so the search used to fall through to no_results_reason: "location_required" — telling you to send a location you had just sent.
  • radius_miles with no origin throws. There is nothing to measure from on a city/state scope, so the radius was ignored and a store several states away came back for radius_miles: 1. Pair radius_miles with latitude/longitude, or with a postal_code (or an address whose ZIP resolves to a centroid).
Search behavior:
  • A location is required for restaurant results. A query with no location returns brand_matches summaries instead (see below) — never a list of arbitrary far-away stores.
  • query matches 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 carries match_score (0-100) and matched_on (name | brand | cuisine | description | address).
  • Default ordering: relevance when query is present (distance, then name, as tie-breaks); distance when an origin resolved; name otherwise. sort overrides. sort: "prep_time" treats 0/unpublished prep as unknown (sorted last).
  • Pagination — offset skips ranked results; the envelope always carries total_matches (pre-pagination count) and next_offset when more pages exist. An offset past the end returns an empty page without a no_results_reason — reasons describe the match set, not the page.
  • Projection — fields returns only the named fields per restaurant (id is always included, and match_score/matched_on always survive when a query ran; unknown names are ignored). Use it for cheap disambiguation calls before pulling full entries.
  • collapse_brands: true returns only the best-ranked location per brand (nearest under distance sort, most relevant under relevance sort), annotated with brand_location_count. Independents always pass.
  • limit defaults to 20, max 50.
Availability annotation — 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 when fulfillment_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: true hard-filters to open stores at requested_time — or at the current time when sent alone. open_now: true is the same filter evaluated at the current time (its orderable flag skips the lead floor — ASAP orders resolve their own time). Combining open_now with requested_time is contradictory and rejected with an error. The envelope’s evaluated_time echoes the instant used.
  • earliest_orderable_time (present on every full entry, may be null; like any other field it must be named in fields to survive a projection) — the soonest ISO instant an order could be scheduled for: now + the exact server lead floor (prep + busy delay, courier floor when fulfillment_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.
Declarative filters, all optional: 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:
  • agent is 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 at limit: 50 the per-entry copies cost roughly 3.5k tokens of the same object. It is not returned inside restaurants[] any more, and fields cannot reintroduce it. (The internal REST listing still carries it per entry; only the MCP tool changed.)
  • origin is present whenever a search origin resolved; source says how.
  • no_results_reason is present only when restaurants is 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 but only_open/open_now removed them all (offer a later time; each removed store’s next_open_at would have said when).
  • evaluated_time is present whenever availability was evaluated (requested_time or open_now).
  • brand_matches (only on location_required with a query) 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.
Restaurant entries carry:
Plus 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) to 4. 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.
Marketplace rows carry 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:
  • query is required. Location follows the same contract as search_restaurants, including the rejected latitude-without-longitude and origin-less radius_miles inputs. Alternatively restaurant_id stands in for a location (“does this store have X”) and ignores the location fields entirely — including those two guards.
  • fulfillment_type is 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 get no_results_reason: "fulfillment_type_not_supported", never a misleading no_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_score and matched_on (name | tags | category | description).
  • Only currently-available items are returned. requested_time additionally annotates available_at_requested_time from the item’s own availability windows (menu/category hours) in the restaurant’s timezone, and puts open_at_requested_time on the item’s restaurant block — 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; limit caps the total (default 20, max 50); max_price_cents filters 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_code when coverage matters). restaurants_scanned / restaurants_truncated report the actual coverage. A store whose menu fails to load is skipped, never fatal.
  • Sort: match_score desc, then restaurant distance, then price, then name.
Output:
Pagination works exactly as on 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-check validate_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 than horizon_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; days is a real per-day layout you can offer a user, not a single entry spanning the whole horizon.
  • Empty days with 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_time about 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 to collapse_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:
Unknown brands return { "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 when capabilities.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 on provider_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:
Show the candidates to the consumer; only after they confirm the exact one, call save_delivery_address with its placeId.

save_delivery_address

Account-wide write on provider_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 — a find_address_candidates candidate the consumer confirmed. Saves a new address (no dedupe — check list_saved_addresses first). Optional: description, address_type (house, apartment, hotel, office, other), subpremise, delivery_instructions.
  • address_id — an existing entry from list_saved_addresses to select as default.
Input: { "confirmed": true, "place_id": "dXJuOm1ieGFkcjoyZjM4Y2I5Ny0…", "subpremise": "Suite 200" } Output: the refreshed list_saved_addresses payload, with the new or selected entry carrying isDefault: true.