Skip to main content
Glama
restaurants.ts7.89 kB
// Local JSON database; geocoding via OpenStreetMap Nominatim for city/state → lat/lon export type Restaurant = { id: string; name: string; rating?: number; price?: string; categories?: { alias: string; title: string }[]; location?: { address1?: string; city?: string }; website?: string; review_count?: number; latitude?: number; longitude?: number; phone?: string; hours?: string[]; brandColor?: string; }; import db from "../data/restaurants.json" with { type: "json" }; const DB_RESTAURANTS: Restaurant[] = db as any as Restaurant[]; // Synonyms for better matching across sources and to resolve category intent const TERM_SYNONYMS: Record<string, string[]> = { bbq: [ "bbq", "barbecue", "bar-b-q", "bar-b-que", "bar b q", "smokehouse", "smoked", ], coffee: ["coffee", "espresso", "cafe", "café", "cafeteria"], mexican: ["mexican", "tex-mex", "tacos"], tacos: ["tacos", "taco", "mexican"], sushi: ["sushi", "japanese", "omakase", "izakaya"], pizza: ["pizza", "pizzeria", "slice"], breakfast: ["breakfast", "brunch", "waffle", "pancake"], italian: ["italian", "pasta", "trattoria", "osteria"], seafood: ["seafood", "oyster", "lobster", "fish"], burgers: ["burgers", "burger", "smash"], bakery: ["bakery", "boulangerie", "patisserie", "bakeshop"], }; const KNOWN_CATEGORY_ALIASES = new Set(Object.keys(TERM_SYNONYMS)); function expandTerms(raw: string): Set<string> { const needle = (raw || "").toLowerCase().trim(); const expanded = new Set<string>([needle]); for (const list of Object.values(TERM_SYNONYMS)) { if (needle && list.some((t) => needle.includes(t))) { list.forEach((t) => expanded.add(t)); } } return expanded; } function resolveCanonicalCategoryAlias(raw: string): string | null { const needle = (raw || "").toLowerCase().trim(); if (!needle) return null; // Check each alias and its synonyms to see if the query implies that category for (const [alias, synonyms] of Object.entries(TERM_SYNONYMS)) { if (alias === needle || synonyms.some((t) => needle.includes(t))) { return alias; } } return null; } // Haversine to filter by proximity function haversineKm( lat1: number, lon1: number, lat2: number, lon2: number ): number { const R = 6371; // km const dLat = ((lat2 - lat1) * Math.PI) / 180; const dLon = ((lon2 - lon1) * Math.PI) / 180; const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos((lat1 * Math.PI) / 180) * Math.cos((lat2 * Math.PI) / 180) * Math.sin(dLon / 2) * Math.sin(dLon / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; } type SearchParams = { term?: string; city?: string; state?: string; latitude?: number; longitude?: number; limit?: number; }; async function geocodeCityState( city?: string, state?: string ): Promise<{ lat: number; lon: number } | null> { const input = [city?.trim(), state?.trim()].filter(Boolean).join(", "); if (!input) return null; const url = `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent( input )}`; try { const resp = await fetch(url, { headers: { "User-Agent": process.env.MCP_GEOCODE_USERAGENT || "mcp-agentic-commerce/1.0 (+https://squareup.com)", }, } as any); if (!resp.ok) return null; const json = (await resp.json()) as Array<{ lat: string; lon: string; }>; if (!json?.length) return null; const first = json[0]; const lat = Number(first.lat); const lon = Number(first.lon); if (Number.isFinite(lat) && Number.isFinite(lon)) return { lat, lon }; return null; } catch { return null; } } export async function searchRestaurants(params: SearchParams) { const { term, city, state } = params || {}; let { latitude, longitude } = params || {}; const limit = typeof params?.limit === "number" ? params.limit : 10; // Resolve coordinates if not provided if ( (typeof latitude !== "number" || typeof longitude !== "number") && (city || state) ) { const geo = await geocodeCityState(city, state); if (geo) { latitude = geo.lat; longitude = geo.lon; } else { const err: any = new Error( `Unable to resolve location from city/state: ${ [city, state].filter(Boolean).join(", ") || "<missing>" }` ); err.code = "GEOCODE_NOT_FOUND"; throw err; } } // If still no coordinates (and no city/state provided), throw if (typeof latitude !== "number" || typeof longitude !== "number") { const err: any = new Error( "Location is required (provide city/state or latitude/longitude)." ); err.code = "LOCATION_REQUIRED"; throw err; } const normalizedTerm = (term || "").trim(); const needle = normalizedTerm.toLowerCase(); const primaryCategoryAlias = resolveCanonicalCategoryAlias(needle); const expandedTerms = expandTerms(needle); const byText = (b: Restaurant) => Array.from(expandedTerms).some((t) => !t ? true : [ b.name, ...(b.categories || []).map((c) => c.title), b.location?.city || "", ] .join(" ") .toLowerCase() .includes(t) ); // Proximity pool within ~30km when lat/lon supplied, otherwise whole DB const candidates = DB_RESTAURANTS.map((r) => ({ r, d: typeof r.latitude === "number" && typeof r.longitude === "number" ? haversineKm(latitude, longitude, r.latitude, r.longitude) : Infinity, })); const nearby = candidates .filter((x) => x.d <= 30 || !isFinite(x.d)) .sort((a, b) => a.d - b.d) .map((x) => x.r); // Only use nearby results; if none found, return an empty set so the UI // can communicate no local results instead of showing another city. const pool = nearby; const filtered = pool.filter((b) => { if ( primaryCategoryAlias && KNOWN_CATEGORY_ALIASES.has(primaryCategoryAlias) ) { const primary = (b.categories || [])[0]; const primaryAlias = (primary?.alias || "").toLowerCase(); const primaryTitle = (primary?.title || "").toLowerCase(); // Enforce primary category match only return ( primaryAlias === primaryCategoryAlias || primaryTitle.includes(primaryCategoryAlias) ); } return needle ? byText(b) : true; }); const deduped = new Map<string, Restaurant>(); for (const b of filtered) deduped.set(b.id, b); const finalList = Array.from(deduped.values()).slice(0, limit); return { businesses: finalList, meta: { source: "local-db", total: finalList.length }, suggestions: [ "Try specifying a cuisine like 'bbq', 'tacos', 'sushi', or 'pizza'", "Include a neighborhood or district (e.g., 'downtown', 'city center', or a postal code)", "Add a price hint like '$$' or a rating target like '4.5+' in the query", ], } as any; } export async function getRestaurant(id: string) { const found = DB_RESTAURANTS.find((b) => b.id === id); return found ?? {}; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aharvard/mcp_agentic-commerce'

If you have feedback or need assistance with the MCP directory API, please join our Discord server