ai_top_pages
Retrieve specific pages cited by AI models for a keyword, providing granular insights beyond domain-level data to refine local SEO strategies.
Instructions
Get the top pages (not just domains) cited by AI models for a keyword. More granular than top_sources. Costs 5 credits.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| keyword | Yes | Keyword to search for (e.g. "best dentist") | |
| location | No | Location for results (e.g. "Chicago, IL"). Default: US | |
| platforms | No | Platforms to query. Default: all | |
| limit | No | Max pages. Default: 10, max: 50 |
Implementation Reference
- src/server.ts:13-13 (registration)Import of registerAIVisibilityTools function, which registers the ai_top_pages tool among others.
import { registerAIVisibilityTools } from "./tools/ai-visibility.js"; - src/server.ts:45-45 (registration)Invocation of registerAIVisibilityTools to register all AI visibility tools including ai_top_pages on the MCP server.
registerAIVisibilityTools(server, getAuth); - src/tools/ai-visibility.ts:52-70 (handler)The ai_top_pages tool definition and handler. Calls GET /v1/ai/top-pages API with keyword, location, platforms, and limit parameters. Returns formatted JSON result.
server.tool( "ai_top_pages", "Get the top pages (not just domains) cited by AI models for a keyword. More granular than top_sources. Costs 5 credits.", { keyword: z.string().min(1).describe('Keyword to search for (e.g. "best dentist")'), location: z.string().optional().describe('Location for results (e.g. "Chicago, IL"). Default: US'), platforms: z.array(z.enum(["chat_gpt", "google"])).optional().describe("Platforms to query. Default: all"), limit: z.number().int().min(1).max(50).optional().describe("Max pages. Default: 10, max: 50"), }, READ_ONLY, withErrorHandling(async ({ keyword, location, platforms, limit }) => { const result = await callApi( "/v1/ai/top-pages", { keyword, ...(location && { location }), ...(platforms && { platforms }), ...(limit && { limit }) }, getAuth() ); return { content: [{ type: "text" as const, text: formatResult(result.data, result) }] }; }) ); - src/tools/ai-visibility.ts:55-60 (schema)Schema definition for ai_top_pages tool inputs: keyword (required string), location, platforms (array of chat_gpt/google), and limit (1-50).
{ keyword: z.string().min(1).describe('Keyword to search for (e.g. "best dentist")'), location: z.string().optional().describe('Location for results (e.g. "Chicago, IL"). Default: US'), platforms: z.array(z.enum(["chat_gpt", "google"])).optional().describe("Platforms to query. Default: all"), limit: z.number().int().min(1).max(50).optional().describe("Max pages. Default: 10, max: 50"), }, - src/api-client.ts:25-92 (helper)The callApi helper function that makes the actual POST request to the API endpoint. Used by the ai_top_pages handler to call /v1/ai/top-pages.
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, }; }