related_keywords
Get semantically related keywords and metrics for up to 20 seed keywords in a specific geographic location.
Instructions
Find related keywords for up to 20 seed keywords. Returns semantically related keywords with metrics. Costs 2 credits.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| keywords | Yes | Array of seed keywords | |
| location | Yes | Geographic location (e.g. "Orchard Park, NY") | |
| limit | No | Max results. Default: 50, max: 1000 |
Implementation Reference
- src/tools/keywords.ts:56-73 (handler)The 'related_keywords' tool handler function registered via server.tool(). It accepts keywords (array, max 20), location, and optional limit, then calls the API endpoint /v1/keywords/related and formats the result.
server.tool( "related_keywords", "Find related keywords for up to 20 seed keywords. Returns semantically related keywords with metrics. Costs 2 credits.", { keywords: z.array(z.string().min(1)).min(1).max(20).describe("Array of seed keywords"), location: z.string().min(1).describe('Geographic location (e.g. "Orchard Park, NY")'), limit: z.number().int().min(1).max(1000).optional().describe("Max results. Default: 50, max: 1000"), }, READ_ONLY, withErrorHandling(async ({ keywords, location, limit }) => { const result = await callApi( "/v1/keywords/related", { keywords, location, ...(limit && { limit }) }, getAuth() ); return { content: [{ type: "text" as const, text: formatResult(result.data, result) }] }; }) ); - src/tools/keywords.ts:59-63 (schema)Input schema for the related_keywords tool using Zod: keywords (array of strings, 1-20), location (string), limit (optional integer 1-1000).
{ keywords: z.array(z.string().min(1)).min(1).max(20).describe("Array of seed keywords"), location: z.string().min(1).describe('Geographic location (e.g. "Orchard Park, NY")'), limit: z.number().int().min(1).max(1000).optional().describe("Max results. Default: 50, max: 1000"), }, - src/tools/keywords.ts:11-112 (registration)The registerKeywordTools function that registers all keyword-related tools (including related_keywords) on the MCP server.
export function registerKeywordTools(server: McpServer, getAuth: () => string) { server.tool( "search_volume", "Get search volume and keyword metrics for up to 1000 keywords. Returns monthly search volume, CPC, competition, and trend data. Costs 1 credit per 50 keywords.", { keywords: z.array(z.string().min(1)).min(1).max(1000).describe("Array of keywords to analyze"), location: z.string().min(1).describe('Geographic location (e.g. "Orchard Park, NY")'), language: z.string().optional().describe('Language code. Default: "en"'), }, READ_ONLY, withErrorHandling(async ({ keywords, location, language }) => { const result = await callApi( "/v1/keywords/search-volume", { keywords, location, ...(language && { language }) }, getAuth() ); return { content: [{ type: "text" as const, text: formatResult(result.data, result) }] }; }) ); server.tool( "keyword_suggestions", "Get keyword suggestions for a seed keyword. Returns related keywords with search volume and metrics. Costs 2 credits.", { keyword: z.string().min(1).describe('Seed keyword (e.g. "plumber")'), location: z.string().min(1).describe('Geographic location (e.g. "Orchard Park, NY")'), limit: z.number().int().min(1).max(1000).optional().describe("Max suggestions. Default: 50, max: 1000"), include_seed_keyword: z.boolean().optional().describe("Include seed keyword in results. Default: true"), }, READ_ONLY, withErrorHandling(async ({ keyword, location, limit, include_seed_keyword }) => { const result = await callApi( "/v1/keywords/suggestions", { keyword, location, ...(limit && { limit }), ...(include_seed_keyword !== undefined && { include_seed_keyword }), }, getAuth() ); return { content: [{ type: "text" as const, text: formatResult(result.data, result) }] }; }) ); server.tool( "related_keywords", "Find related keywords for up to 20 seed keywords. Returns semantically related keywords with metrics. Costs 2 credits.", { keywords: z.array(z.string().min(1)).min(1).max(20).describe("Array of seed keywords"), location: z.string().min(1).describe('Geographic location (e.g. "Orchard Park, NY")'), limit: z.number().int().min(1).max(1000).optional().describe("Max results. Default: 50, max: 1000"), }, READ_ONLY, withErrorHandling(async ({ keywords, location, limit }) => { const result = await callApi( "/v1/keywords/related", { keywords, location, ...(limit && { limit }) }, getAuth() ); return { content: [{ type: "text" as const, text: formatResult(result.data, result) }] }; }) ); server.tool( "keywords_for_site", "Get keywords a domain currently ranks for. Returns keywords with rank positions and search volume. Costs 3 credits.", { domain: z.string().min(1).describe('Domain to analyze (e.g. "example.com")'), location: z.string().min(1).describe('Geographic location (e.g. "Orchard Park, NY")'), limit: z.number().int().min(1).max(1000).optional().describe("Max results. Default: 50, max: 1000"), }, READ_ONLY, withErrorHandling(async ({ domain, location, limit }) => { const result = await callApi( "/v1/keywords/for-site", { domain, location, ...(limit && { limit }) }, getAuth() ); return { content: [{ type: "text" as const, text: formatResult(result.data, result) }] }; }) ); server.tool( "keyword_trends", "Get search trend data for up to 5 keywords over time. Periods: 3m, 6m, 12m (default), 5y. Costs 1 credit.", { keywords: z.array(z.string().min(1)).min(1).max(5).describe("Keywords to get trends for"), location: z.string().min(1).describe('Geographic location (e.g. "Orchard Park, NY")'), period: z.enum(["3m", "6m", "12m", "5y"]).optional().describe('Time period. Default: "12m"'), }, READ_ONLY, withErrorHandling(async ({ keywords, location, period }) => { const result = await callApi( "/v1/keywords/trends", { keywords, location, ...(period && { period }) }, getAuth() ); return { content: [{ type: "text" as const, text: formatResult(result.data, result) }] }; }) ); } - src/api-client.ts:143-158 (helper)The withErrorHandling wrapper used to wrap the handler, ensuring thrown errors are surfaced 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, }; } }; } - src/api-client.ts:132-138 (helper)The formatResult helper used to format the API response data into a text output with credits metadata.
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)}`; }