geogrid_scan
Check local search rankings across a geographic area by scanning a grid of points around a business location.
Instructions
Run a geogrid rank scan to see where a business ranks across a geographic area. Creates a grid of points around the location and checks the business's rank at each point. Returns: grid (2D array of rank numbers indexed by row/col, 0 = not found), grid_points (flat array where each entry has row, col, lat, lng, rank — use this when the user asks to plot the grid on a map), center ({lat, lng}), average_rank, found_in, total_points. Costs 50 credits (5x5), 98 credits (7x7), or 162 credits (9x9). This is an async operation — the tool will poll until results are ready.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| business | Yes | Business name to track | |
| place_id | No | Google Place ID for precise matching (e.g. ChIJ...). When provided, the business is matched by Place ID instead of name, which is far more reliable. | |
| location | Yes | Center location for the grid | |
| keyword | Yes | Search keyword to check rankings for | |
| grid_size | No | Grid dimensions. Default: 5x5 | |
| radius_miles | No | Radius in miles from center. Default: 3 |
Implementation Reference
- src/tools/geogrid.ts:28-98 (handler)The main handler function for the geogrid_scan tool. Submits a geogrid scan job via POST /v1/geogrid/scan, then polls GET /v1/geogrid/status/{jobId} every 3 seconds (up to 120s timeout) for completion. Returns the grid data on success, error on failure, or a partial status message on timeout.
withErrorHandling(async ({ business, place_id, location, keyword, grid_size, radius_miles }) => { // Submit the scan job const submitResult = await callApi( "/v1/geogrid/scan", { business, ...(place_id && { place_id }), location, keyword, ...(grid_size && { grid_size }), ...(radius_miles && { radius_miles }), }, getAuth() ); const jobData = submitResult.data as Record<string, unknown>; // If sandbox mode returned complete results immediately if (jobData.status === "complete") { return { content: [{ type: "text" as const, text: formatResult(jobData, submitResult) }] }; } const jobId = jobData.job_id as string; // Poll for results const maxWaitMs = 120_000; const pollIntervalMs = 3_000; const startTime = Date.now(); while (Date.now() - startTime < maxWaitMs) { await sleep(pollIntervalMs); const statusResult = await callApiGet( `/v1/geogrid/status/${jobId}`, getAuth() ); const statusData = statusResult.data as Record<string, unknown>; if (statusData.status === "complete") { return { content: [{ type: "text" as const, text: formatResult(statusData, { credits_used: submitResult.credits_used, credits_remaining: statusResult.credits_remaining, cached: false, }), }], }; } if (statusData.status === "failed") { return { content: [{ type: "text" as const, text: `Geogrid scan failed: ${statusData.error || "Unknown error"}`, }], isError: true, }; } } // Timeout — return partial status return { content: [{ type: "text" as const, text: `Geogrid scan is still running (job_id: ${jobId}). You can check status later. ${submitResult.credits_used} credits were used.`, }], }; }) - src/tools/geogrid.ts:19-26 (schema)Input schema for geogrid_scan using Zod. Parameters: business (string), place_id (optional string), location (string), keyword (string), grid_size (optional enum: 5x5/7x7/9x9, default 5x5), radius_miles (optional positive number, default 3).
{ business: z.string().describe("Business name to track"), place_id: z.string().optional().describe("Google Place ID for precise matching (e.g. ChIJ...). When provided, the business is matched by Place ID instead of name, which is far more reliable."), location: z.string().describe("Center location for the grid"), keyword: z.string().describe("Search keyword to check rankings for"), grid_size: z.enum(["5x5", "7x7", "9x9"]).optional().describe("Grid dimensions. Default: 5x5"), radius_miles: z.number().positive().optional().describe("Radius in miles from center. Default: 3"), }, - src/tools/geogrid.ts:15-99 (registration)Registration of geogrid_scan via server.tool() inside registerGeogridTools, which is called from server.ts at line 39. The tool description explains the return format, credit costs (50/98/162 credits), and async polling behavior.
export function registerGeogridTools(server: McpServer, getAuth: () => string) { server.tool( "geogrid_scan", "Run a geogrid rank scan to see where a business ranks across a geographic area. Creates a grid of points around the location and checks the business's rank at each point. Returns: `grid` (2D array of rank numbers indexed by row/col, 0 = not found), `grid_points` (flat array where each entry has `row`, `col`, `lat`, `lng`, `rank` — use this when the user asks to plot the grid on a map), `center` ({lat, lng}), `average_rank`, `found_in`, `total_points`. Costs 50 credits (5x5), 98 credits (7x7), or 162 credits (9x9). This is an async operation — the tool will poll until results are ready.", { business: z.string().describe("Business name to track"), place_id: z.string().optional().describe("Google Place ID for precise matching (e.g. ChIJ...). When provided, the business is matched by Place ID instead of name, which is far more reliable."), location: z.string().describe("Center location for the grid"), keyword: z.string().describe("Search keyword to check rankings for"), grid_size: z.enum(["5x5", "7x7", "9x9"]).optional().describe("Grid dimensions. Default: 5x5"), radius_miles: z.number().positive().optional().describe("Radius in miles from center. Default: 3"), }, READ_ONLY, withErrorHandling(async ({ business, place_id, location, keyword, grid_size, radius_miles }) => { // Submit the scan job const submitResult = await callApi( "/v1/geogrid/scan", { business, ...(place_id && { place_id }), location, keyword, ...(grid_size && { grid_size }), ...(radius_miles && { radius_miles }), }, getAuth() ); const jobData = submitResult.data as Record<string, unknown>; // If sandbox mode returned complete results immediately if (jobData.status === "complete") { return { content: [{ type: "text" as const, text: formatResult(jobData, submitResult) }] }; } const jobId = jobData.job_id as string; // Poll for results const maxWaitMs = 120_000; const pollIntervalMs = 3_000; const startTime = Date.now(); while (Date.now() - startTime < maxWaitMs) { await sleep(pollIntervalMs); const statusResult = await callApiGet( `/v1/geogrid/status/${jobId}`, getAuth() ); const statusData = statusResult.data as Record<string, unknown>; if (statusData.status === "complete") { return { content: [{ type: "text" as const, text: formatResult(statusData, { credits_used: submitResult.credits_used, credits_remaining: statusResult.credits_remaining, cached: false, }), }], }; } if (statusData.status === "failed") { return { content: [{ type: "text" as const, text: `Geogrid scan failed: ${statusData.error || "Unknown error"}`, }], isError: true, }; } } // Timeout — return partial status return { content: [{ type: "text" as const, text: `Geogrid scan is still running (job_id: ${jobId}). You can check status later. ${submitResult.credits_used} credits were used.`, }], }; }) ); - src/api-client.ts:143-158 (helper)withErrorHandling wraps the handler to catch thrown errors and return them as MCP error content. Also relevant: callApi (POST helper), callApiGet (GET helper), formatResult (JSON formatting with credit metadata), and sleep utility in geogrid.ts (line 11).
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, }; } }; }