profile_health
Audit a Google Business Profile to check completeness, identify missing or incomplete fields, and receive actionable recommendations for improvement.
Instructions
Audit a Google Business Profile's completeness. Returns a completeness score, missing/incomplete fields, and actionable recommendations. Costs 2 credits.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| business_name | Yes | Business name | |
| location | Yes | City and state | |
| place_id | No | Google Place ID for exact match |
Implementation Reference
- src/tools/business.ts:31-48 (handler)The handler function for the 'profile_health' tool. It calls the '/v1/profile/health' API endpoint with business_name, location, and optional place_id, then formats and returns the result. The tool audits a Google Business Profile's completeness, returning a completeness score, missing/incomplete fields, and actionable recommendations.
server.tool( "profile_health", "Audit a Google Business Profile's completeness. Returns a completeness score, missing/incomplete fields, and actionable recommendations. Costs 2 credits.", { business_name: z.string().describe("Business name"), location: z.string().describe("City and state"), place_id: z.string().optional().describe("Google Place ID for exact match"), }, READ_ONLY, withErrorHandling(async ({ business_name, location, place_id }) => { const result = await callApi( "/v1/profile/health", { business_name, location, ...(place_id && { place_id }) }, getAuth() ); return { content: [{ type: "text" as const, text: formatResult(result.data, result) }] }; }) ); - src/tools/business.ts:34-38 (schema)Input schema for 'profile_health' tool using Zod: business_name (string), location (string), and optional place_id (string). These define what the user must provide to run the tool.
{ business_name: z.string().describe("Business name"), location: z.string().describe("City and state"), place_id: z.string().optional().describe("Google Place ID for exact match"), }, - src/tools/business.ts:11-87 (registration)The registerBusinessTools function registers the 'profile_health' tool (along with business_profile, qa, and business_listings) on the MCP server via server.tool(). This is called from src/server.ts line 36.
export function registerBusinessTools(server: McpServer, getAuth: () => string) { server.tool( "business_profile", "Get a complete Google Business Profile including name, rating, reviews, address, phone, website, hours, categories, attributes, photos count, description, and verification status. Costs 2 credits.", { business_name: z.string().describe("Business name"), location: z.string().describe("City and state"), place_id: z.string().optional().describe("Google Place ID for exact match"), }, READ_ONLY, withErrorHandling(async ({ business_name, location, place_id }) => { const result = await callApi( "/v1/business/profile", { business_name, location, ...(place_id && { place_id }) }, getAuth() ); return { content: [{ type: "text" as const, text: formatResult(result.data, result) }] }; }) ); server.tool( "profile_health", "Audit a Google Business Profile's completeness. Returns a completeness score, missing/incomplete fields, and actionable recommendations. Costs 2 credits.", { business_name: z.string().describe("Business name"), location: z.string().describe("City and state"), place_id: z.string().optional().describe("Google Place ID for exact match"), }, READ_ONLY, withErrorHandling(async ({ business_name, location, place_id }) => { const result = await callApi( "/v1/profile/health", { business_name, location, ...(place_id && { place_id }) }, getAuth() ); return { content: [{ type: "text" as const, text: formatResult(result.data, result) }] }; }) ); server.tool( "qa", "Get Questions & Answers from a Google Business Profile. Returns questions with their answers, authors, dates, and upvotes. Costs 1 credit.", { business_name: z.string().describe("Business name"), location: z.string().describe("City and state"), place_id: z.string().optional().describe("Google Place ID for exact match"), }, READ_ONLY, withErrorHandling(async ({ business_name, location, place_id }) => { const result = await callApi( "/v1/business/qa", { business_name, location, ...(place_id && { place_id }) }, getAuth() ); return { content: [{ type: "text" as const, text: formatResult(result.data, result) }] }; }) ); 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)Where registerBusinessTools is called to register all business tools including 'profile_health' on the MCP server.
registerBusinessTools(server, getAuth); - src/api-client.ts:25-92 (helper)The callApi function used by the handler to make POST requests to the API, along with withErrorHandling (lines 143-158) which wraps handlers to catch errors and return them as MCP error content.
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, }; }