search_businesses
Search businesses by category and location. Use filters for country, language, subcategory, and minimum rating to get ranked results with names, ratings, and match scores.
Instructions
Search businesses by category and location. Returns ranked hits with name, city, rating, and matchScore. Filters: countryCode, language, subcategory, minRating, maxResults.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| category | Yes | Vertical to search. One of: realtor, insurance_agent, medical_practitioner, dentist, home_health, medical_transport, home_services. Or a free-text category like 'plumber'. | |
| location | Yes | Location string — city, region, postal code, or 'Dallas, TX' style. Geocoded server-side. | |
| countryCode | No | ISO-3166 alpha-2 (US, CA, GB, AU). Restricts results to that country. | |
| language | No | ISO-639-1 (en, fr, es, ...). Boosts businesses speaking this language. | |
| subcategory | No | Optional sub-tag, e.g. 'buyer-agent', 'auto-insurance', 'family-medicine'. | |
| maxResults | No | ||
| minRating | No |
Implementation Reference
- src/tools/searchBusinesses.ts:38-101 (handler)Main handler that loads all businesses, optionally filters by countryCode/minRating/category/subcategory, scores each against the input context, sorts by score, and returns the top results.
export async function searchBusinesses(input: SearchBusinessesInput): Promise<SearchHit[]> { const businesses = await getAllBusinesses(); const origin = input.location ? await geocode(input.location) : null; let candidates = businesses; if (input.countryCode) { candidates = candidates.filter((b) => b.address.countryCode === input.countryCode); } if (input.minRating != null) { candidates = candidates.filter((b) => { const top = b.publicReviews?.[0]; return top ? top.rating >= input.minRating! : false; }); } // Treat the input.category as either a known Vertical or a subcategory. const KNOWN: Vertical[] = [ "realtor", "insurance_agent", "medical_practitioner", "dentist", "home_health", "medical_transport", "home_services" ]; const isVertical = (KNOWN as string[]).includes(input.category); if (isVertical) { candidates = candidates.filter((b) => b.vertical === input.category); } else { const cat = input.category.toLowerCase(); candidates = candidates.filter( (b) => b.subcategories.some((s) => s.toLowerCase().includes(cat)) || b.servicesOffered.some((s) => s.toLowerCase().includes(cat)) ); } if (input.subcategory) { const sub = input.subcategory.toLowerCase(); candidates = candidates.filter((b) => b.subcategories.some((s) => s.toLowerCase().includes(sub)) ); } const ctx = { origin: origin ?? undefined, vertical: isVertical ? input.category : undefined, subcategory: input.subcategory ?? (!isVertical ? input.category : undefined), language: input.language }; 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, ctx), tier: b.tier })); return sortByScore(scored).slice(0, input.maxResults); } - src/tools/searchBusinesses.ts:14-34 (schema)Zod schema defining the input parameters: category, location, countryCode, language, subcategory, maxResults, minRating.
export const searchBusinessesSchema = z.object({ category: z .string() .describe( "Vertical to search. One of: realtor, insurance_agent, medical_practitioner, dentist, home_health, medical_transport, home_services. Or a free-text category like 'plumber'." ), location: z .string() .describe( "Location string — city, region, postal code, or 'Dallas, TX' style. Geocoded server-side." ), countryCode: z .string() .length(2) .optional() .describe("ISO-3166 alpha-2 (US, CA, GB, AU). Restricts results to that country."), language: z.string().optional().describe("ISO-639-1 (en, fr, es, ...). Boosts businesses speaking this language."), subcategory: z.string().optional().describe("Optional sub-tag, e.g. 'buyer-agent', 'auto-insurance', 'family-medicine'."), maxResults: z.number().int().min(1).max(25).default(10), minRating: z.number().min(0).max(5).optional() }); - src/server.ts:25-33 (registration)Registers the search_businesses tool on the MCP server with a description, schema, and handler callback.
server.tool( "search_businesses", "Search businesses by category and location. Returns ranked hits with name, city, rating, and matchScore. Filters: countryCode, language, subcategory, minRating, maxResults.", searchBusinessesSchema.shape, async (args) => { const hits = await searchBusinesses(searchBusinessesSchema.parse(args)); return { content: [{ type: "text", text: JSON.stringify(hits, null, 2) }] }; } ); - src/http.ts:75-97 (helper)HTTP preview endpoint that wraps the searchBusinesses handler for testing without an MCP client.
// Convenience preview endpoint — lets you test ranking without the MCP client. // e.g. GET /preview/search?category=realtor&location=Dallas app.get("/preview/search", async (req, res) => { try { const hits = await searchBusinesses({ category: String(req.query.category ?? "realtor"), location: String(req.query.location ?? "Dallas"), countryCode: req.query.countryCode ? String(req.query.countryCode) : undefined, language: req.query.language ? String(req.query.language) : undefined, subcategory: req.query.subcategory ? String(req.query.subcategory) : undefined, maxResults: req.query.maxResults ? Number(req.query.maxResults) : 10, minRating: req.query.minRating ? Number(req.query.minRating) : undefined }); res.json({ count: hits.length, hits }); } catch (err) { res.status(400).json({ error: String(err) }); } }); // Stripe webhook stub — wire in Phase 2. app.post("/webhooks/stripe", (_req, res) => { res.status(202).json({ received: true, note: "stub — wire in Phase 2" }); });