search_restaurants
Return restaurants the calling agent is allowed to discover and order from, relevance-ranked when aquery is present.
REST equivalent: GET /api/agent/restaurants (same parameters; responds with the bare restaurants array — the envelope fields below are MCP-only, except pagination metadata, which REST exposes as X-Total-Matches and X-Next-Offset response headers, both CORS-exposed. REST boolean query params accept true/false/1/0; anything else is ignored).
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).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; otherwise it falls back to city-fragment string matching.
- 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. pos_provider/exclude_pos_provideraccept a single provider or an array.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 (within 8 days); 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, 15-minute pickup minimum) 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(always present, may be null) — 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 coming week, 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:
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 permission is not allowed, 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.
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. MCP-only (no REST equivalent). Input:queryis required. Location follows the same contract assearch_restaurants; alternativelyrestaurant_idstands in for a location (“does this store have X”) and ignores the location fields —fulfillment_typeis still honored on that path (a delivery search against a pickup-only store returns nothing).- 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 the nearest 25 stores in scope;
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; a platform configured with no horizon cap lays out at most 14 days). 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. hours_known: false(no stored hours) yields one open-ended window from the earliest orderable instant to the 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.
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.