business_listings
Search for businesses by category and location. Retrieve names, ratings, reviews, addresses, phone numbers, and categories for local SEO analysis.
Instructions
Search for businesses by category and location. Returns a list of businesses with name, rating, reviews, address, phone, place_id, and categories. Costs 10 credits per 50 results.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| category | Yes | Business category (e.g. "plumber", "dentist") | |
| location | Yes | City and state | |
| limit | No | Number of results (1-200). Default: 50 |
Implementation Reference
- src/tools/business.ts:69-86 (handler)The 'business_listings' tool handler. Calls /v1/business/listings API with category, location, and optional limit. Returns up to 200 results (default 50). Costs 10 credits per 50 results.
server.tool( "business_listings", "Search for businesses by category and location. Returns a list of businesses with name, rating, reviews, address, phone, place_id, and categories. Costs 10 credits per 50 results.", { category: z.string().describe('Business category (e.g. "plumber", "dentist")'), location: z.string().describe("City and state"), limit: z.number().int().min(1).max(200).optional().describe("Number of results (1-200). Default: 50"), }, READ_ONLY, withErrorHandling(async ({ category, location, limit }) => { const result = await callApi( "/v1/business/listings", { category, location, ...(limit && { limit }) }, getAuth() ); return { content: [{ type: "text" as const, text: formatResult(result.data, result) }] }; }) ); - src/tools/business.ts:72-76 (schema)Input schema for 'business_listings': category (string), location (string), and optional limit (int 1-200, default 50).
{ category: z.string().describe('Business category (e.g. "plumber", "dentist")'), location: z.string().describe("City and state"), limit: z.number().int().min(1).max(200).optional().describe("Number of results (1-200). Default: 50"), }, - src/tools/business.ts:69-86 (registration)Tool registered via server.tool() with the name 'business_listings' and read-only hints.
server.tool( "business_listings", "Search for businesses by category and location. Returns a list of businesses with name, rating, reviews, address, phone, place_id, and categories. Costs 10 credits per 50 results.", { category: z.string().describe('Business category (e.g. "plumber", "dentist")'), location: z.string().describe("City and state"), limit: z.number().int().min(1).max(200).optional().describe("Number of results (1-200). Default: 50"), }, READ_ONLY, withErrorHandling(async ({ category, location, limit }) => { const result = await callApi( "/v1/business/listings", { category, location, ...(limit && { limit }) }, getAuth() ); return { content: [{ type: "text" as const, text: formatResult(result.data, result) }] }; }) ); - src/server.ts:36-36 (registration)Registration call: registerBusinessTools(server, getAuth) wires the tools into the MCP server.
registerBusinessTools(server, getAuth); - src/api-client.ts:25-158 (helper)The callApi helper and withErrorHandling wrapper used by the business_listings handler to call the external API and format results.
export async function callApi( path: string, body: Record<string, unknown>, authHeader: string, timeoutMs = 60_000 ): Promise<{ data: unknown; credits_used: number; credits_remaining: number; cached: boolean }> { const url = `${env.API_BASE_URL}${path}`; console.log(`[api] POST ${url} (timeout: ${timeoutMs / 1000}s, auth: ${authHeader ? `${authHeader.slice(0, 15)}...` : "MISSING"})`); const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", Authorization: authHeader, }, body: JSON.stringify(body), signal: AbortSignal.timeout(timeoutMs), }); if (!response.ok) { const text = await response.text(); console.error(`[api] ${response.status} ${response.statusText} from ${path}: ${text.slice(0, 200)}`); // Try to parse as structured error try { const result = JSON.parse(text) as ApiErrorResponse; if (result.status === "error") { const err = result.error; const reqId = result.request_id ? ` [request_id: ${result.request_id}]` : ""; throw new Error( err.required_credits ? `${err.message} (requires ${err.required_credits} credits, balance: ${err.current_balance})${reqId}` : `${err.message}${reqId}` ); } } catch (parseErr) { if (parseErr instanceof Error && parseErr.message !== "error") { // Re-throw if it's our structured error from above if (!text.includes('"status":"error"')) { throw new Error(`API returned ${response.status}: ${text.slice(0, 200)}`); } throw parseErr; } } throw new Error(`API returned ${response.status}: ${text.slice(0, 200)}`); } const result = (await response.json()) as ApiResponse; if (result.status === "error") { const err = (result as ApiErrorResponse).error; const reqId = (result as ApiErrorResponse).request_id ? ` [request_id: ${(result as ApiErrorResponse).request_id}]` : ""; throw new Error( err.required_credits ? `${err.message} (requires ${err.required_credits} credits, balance: ${err.current_balance})${reqId}` : `${err.message}${reqId}` ); } console.log(`[api] ${path} OK (${result.credits_used} credits used, ${result.credits_remaining} remaining)`); return { data: result.data, credits_used: result.credits_used, credits_remaining: result.credits_remaining, cached: result.cached, }; } export async function callApiGet( path: string, authHeader: string ): Promise<{ data: unknown; credits_used: number; credits_remaining: number; cached: boolean }> { const url = `${env.API_BASE_URL}${path}`; console.log(`[api] GET ${url} (auth: ${authHeader ? `${authHeader.slice(0, 15)}...` : "MISSING"})`); const response = await fetch(url, { method: "GET", headers: { Authorization: authHeader, }, signal: AbortSignal.timeout(60_000), }); if (!response.ok) { const text = await response.text(); console.error(`[api] ${response.status} ${response.statusText} from ${path}: ${text.slice(0, 200)}`); throw new Error(`API returned ${response.status}: ${text.slice(0, 200)}`); } const result = (await response.json()) as ApiResponse; if (result.status === "error") { throw new Error(result.error.message); } console.log(`[api] ${path} OK (${result.credits_used} credits used, ${result.credits_remaining} remaining)`); return { data: result.data, credits_used: result.credits_used, credits_remaining: result.credits_remaining, cached: result.cached, }; } export function formatResult( data: unknown, meta: { credits_used: number; credits_remaining: number; cached: boolean } ): string { const metaLine = `[${meta.credits_used} credit${meta.credits_used !== 1 ? "s" : ""} used | ${meta.credits_remaining} remaining${meta.cached ? " | cached" : ""}]`; return `${metaLine}\n\n${JSON.stringify(data, null, 2)}`; } type ToolResult = { content: { type: "text"; text: string }[]; isError?: boolean }; /** Wrap an MCP tool handler so thrown errors always surface as MCP error content */ export function withErrorHandling<T>( fn: (args: T) => Promise<ToolResult> ): (args: T) => Promise<ToolResult> { return async (args) => { try { return await fn(args); } catch (err) { const message = err instanceof Error ? err.message : String(err); console.error(`[mcp] Tool error: ${message}`); return { content: [{ type: "text" as const, text: `Error: ${message}` }], isError: true, }; } }; }