search_by_query
Find local businesses using natural-language queries. Search by category, location, or fuzzy terms like 'evening dentist that takes Sun Life'.
Instructions
Natural-language search across the catalog. Use for fuzzy queries like 'evening dentist that takes Sun Life' or 'realtor in Dallas who speaks Spanish'.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | Yes | Natural-language query, e.g. 'evening dentist in Toronto that takes Sun Life' or 'realtor in Dallas who speaks Spanish'. | |
| location | No | Optional location override. | |
| countryCode | No | ||
| maxResults | No |
Implementation Reference
- src/tools/searchByQuery.ts:29-59 (handler)Main handler function for the search_by_query tool. Fetches all businesses, optionally filters by countryCode, does token-based keyword matching against the query, scores each business using a weighted ranking algorithm, and returns up to maxResults sorted by matchScore.
export async function searchByQuery(input: SearchByQueryInput): Promise<SearchHit[]> { const businesses = await getAllBusinesses(); let candidates = businesses; if (input.countryCode) { candidates = candidates.filter((b) => b.address.countryCode === input.countryCode); } const origin = input.location ? await geocode(input.location) : null; candidates = candidates.filter( (b) => queryTokenMatch(b, input.query).matched > 0 ); const scored: SearchHit[] = candidates.map((b) => ({ id: b.id, name: b.name, vertical: b.vertical, shortDescription: b.shortDescription, city: b.address.city, countryCode: b.address.countryCode, rating: b.publicReviews?.[0]?.rating, reviewCount: b.publicReviews?.[0]?.count, matchScore: scoreBusiness(b, { origin: origin ?? undefined, query: input.query }), tier: b.tier })); return sortByScore(scored).slice(0, input.maxResults); } - src/tools/searchByQuery.ts:15-25 (schema)Zod schema defining the input parameters for search_by_query. Supports: query (string, min 2 chars), optional location override, optional 2-letter countryCode, and optional maxResults (1-25, default 10).
export const searchByQuerySchema = z.object({ query: z .string() .min(2) .describe( "Natural-language query, e.g. 'evening dentist in Toronto that takes Sun Life' or 'realtor in Dallas who speaks Spanish'." ), location: z.string().optional().describe("Optional location override."), countryCode: z.string().length(2).optional(), maxResults: z.number().int().min(1).max(25).default(10) }); - src/server.ts:55-63 (registration)MCP server registration of the search_by_query tool. Binds the tool name to the schema and handler via server.tool().
server.tool( "search_by_query", "Natural-language search across the catalog. Use for fuzzy queries like 'evening dentist that takes Sun Life' or 'realtor in Dallas who speaks Spanish'.", searchByQuerySchema.shape, async (args) => { const hits = await searchByQuery(searchByQuerySchema.parse(args)); return { content: [{ type: "text", text: JSON.stringify(hits, null, 2) }] }; } ); - src/lib/ranking.ts:101-123 (helper)queryTokenMatch helper used by the handler. Tokenizes the query (splits on non-alphanumeric, removes stop words and short tokens), then counts how many tokens appear anywhere in the business profile (name, description, city, subcategories, services, languages).
export function queryTokenMatch( b: BusinessProfile, query: string ): { matched: number; total: number } { const tokens = query .toLowerCase() .split(/[^a-z0-9]+/) .filter((t) => t.length >= 3 && !STOP_WORDS.has(t)); if (tokens.length === 0) return { matched: 0, total: 0 }; const haystack = [ b.name, b.shortDescription, b.fullDescription, b.address.city, ...b.subcategories, ...b.servicesOffered, ...b.languagesSpoken ] .join(" ") .toLowerCase(); const matched = tokens.filter((t) => haystack.includes(t)).length; return { matched, total: tokens.length }; } - src/lib/ranking.ts:92-94 (helper)sortByScore helper used by the handler to sort search results by matchScore descending.
export function sortByScore<T extends { matchScore: number }>(items: T[]): T[] { return [...items].sort((a, b) => b.matchScore - a.matchScore); }