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 restaurants the calling agent is allowed to discover and order from, relevance-ranked when a query 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:
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).
  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; otherwise it falls back to city-fragment string matching.
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).
  • Paginationoffset 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.
  • Projectionfields 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.
  • pos_provider / exclude_pos_provider accept a single provider or an array.
  • 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 (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 when fulfillment_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: 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 (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 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 coming week, 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:
  • 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 always 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:
  • query is required. Location follows the same contract as search_restaurants; alternatively restaurant_id stands in for a location (“does this store have X”) and ignores the location fields — fulfillment_type is 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_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 the nearest 25 stores in scope; 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; 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 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.

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.