@agentseo/mcp-server
Advanced tools
+107
-22
@@ -10,2 +10,4 @@ #!/usr/bin/env node | ||
| const API_KEY = process.env.AGENTSEO_API_KEY; | ||
| const PROJECT_ID = process.env.AGENTSEO_PROJECT_ID; | ||
| const WORKFLOW_ID = process.env.AGENTSEO_WORKFLOW_ID; | ||
| if (!API_KEY) { | ||
@@ -33,2 +35,4 @@ console.error("Error: AGENTSEO_API_KEY environment variable is required."); | ||
| "Content-Type": "application/json", | ||
| ...(PROJECT_ID ? { "x-project-id": PROJECT_ID } : {}), | ||
| ...(WORKFLOW_ID ? { "x-workflow-id": WORKFLOW_ID } : {}), | ||
| }, | ||
@@ -48,3 +52,5 @@ }); | ||
| return { | ||
| content: [{ type: "text", text: JSON.stringify(payload, null, 2) }], | ||
| content: [ | ||
| { type: "text", text: JSON.stringify(payload, null, 2) }, | ||
| ], | ||
| }; | ||
@@ -54,5 +60,17 @@ } | ||
| query: z.string().describe("The search query"), | ||
| limit: z.number().int().min(1).max(20).default(10).describe("Number of results"), | ||
| include_domains: z.array(z.string()).optional().describe("Restrict results to these domains"), | ||
| exclude_domains: z.array(z.string()).optional().describe("Exclude results from these domains"), | ||
| limit: z | ||
| .number() | ||
| .int() | ||
| .min(1) | ||
| .max(50) | ||
| .default(10) | ||
| .describe("Number of results"), | ||
| include_domains: z | ||
| .array(z.string()) | ||
| .optional() | ||
| .describe("Restrict results to these domains"), | ||
| exclude_domains: z | ||
| .array(z.string()) | ||
| .optional() | ||
| .describe("Exclude results from these domains"), | ||
| sync: z.boolean().default(true).describe("Whether to run synchronously"), | ||
@@ -68,3 +86,6 @@ }, async ({ query, limit, include_domains, exclude_domains, sync }) => { | ||
| url: z.string().url().describe("The URL to scrape"), | ||
| include_images: z.boolean().default(false).describe("Include image links in markdown"), | ||
| include_images: z | ||
| .boolean() | ||
| .default(false) | ||
| .describe("Include image links in markdown"), | ||
| }, async ({ url, include_images }) => { | ||
@@ -74,3 +95,6 @@ const result = await callAgentSeoApi("/extract", { | ||
| }); | ||
| const parsed = z.object({ content: z.string() }).passthrough().parse(result); | ||
| const parsed = z | ||
| .object({ content: z.string() }) | ||
| .passthrough() | ||
| .parse(result); | ||
| return { | ||
@@ -82,4 +106,10 @@ content: [{ type: "text", text: parsed.content }], | ||
| keyword: z.string().describe("Target keyword"), | ||
| location: z.string().default("United States").describe("Geographic location"), | ||
| device: z.enum(["desktop", "mobile"]).default("desktop").describe("Device type"), | ||
| location: z | ||
| .string() | ||
| .default("United States") | ||
| .describe("Geographic location"), | ||
| device: z | ||
| .enum(["desktop", "mobile"]) | ||
| .default("desktop") | ||
| .describe("Device type"), | ||
| language: z.string().default("en").describe("Language code"), | ||
@@ -107,7 +137,10 @@ sync: z.boolean().default(true).describe("Whether to run synchronously"), | ||
| server.tool("agentseo_local_audit_batch", "Queue up to 10 local audits in one request.", { | ||
| items: z.array(z.object({ | ||
| items: z | ||
| .array(z.object({ | ||
| domain: z.string(), | ||
| location: z.string(), | ||
| language: z.string().optional(), | ||
| })).max(10).describe("Audit items"), | ||
| })) | ||
| .max(10) | ||
| .describe("Audit items"), | ||
| }, async ({ items }) => { | ||
@@ -122,5 +155,11 @@ const result = await callAgentSeoApi("/audit/local/batch", { | ||
| keyword: z.string().describe("Target keyword"), | ||
| location: z.string().default("United States").describe("Geographic location"), | ||
| location: z | ||
| .string() | ||
| .default("United States") | ||
| .describe("Geographic location"), | ||
| language: z.string().default("en").describe("Language code"), | ||
| scrape_depth: z.enum(["h1", "h2", "h3"]).default("h3").describe("How deep to analyze headings"), | ||
| scrape_depth: z | ||
| .enum(["h1", "h2", "h3"]) | ||
| .default("h3") | ||
| .describe("How deep to analyze headings"), | ||
| sync: z.boolean().default(true).describe("Whether to run synchronously"), | ||
@@ -134,6 +173,9 @@ }, async ({ url, keyword, location, language, scrape_depth, sync }) => { | ||
| }); | ||
| server.tool("agentseo_social_listen", "Run social listening analysis for a query.", { | ||
| server.tool("agentseo_social_listen", "Find web mentions for a query (web-wide discovery; not platform-native monitoring).", { | ||
| query: z.string().describe("Search query"), | ||
| platform: z.enum(["reddit", "twitter", "all"]).default("all").describe("Platform filter"), | ||
| limit: z.number().int().min(1).max(20).default(10).describe("Result limit"), | ||
| platform: z | ||
| .enum(["reddit", "twitter", "all"]) | ||
| .default("all") | ||
| .describe("Platform filter"), | ||
| limit: z.number().int().min(1).max(50).default(10).describe("Result limit"), | ||
| sync: z.boolean().default(true).describe("Whether to run synchronously"), | ||
@@ -147,10 +189,33 @@ }, async ({ query, platform, limit, sync }) => { | ||
| }); | ||
| // Preferred name; keep agentseo_social_listen for backward compatibility. | ||
| server.tool("agentseo_web_mentions", "Find web mentions for a query (web-wide discovery; not platform-native monitoring).", { | ||
| query: z.string().describe("Search query"), | ||
| platform: z | ||
| .enum(["reddit", "twitter", "all"]) | ||
| .default("all") | ||
| .describe("Platform filter"), | ||
| limit: z.number().int().min(1).max(50).default(10).describe("Result limit"), | ||
| sync: z.boolean().default(true).describe("Whether to run synchronously"), | ||
| }, async ({ query, platform, limit, sync }) => { | ||
| const result = await callAgentSeoApi("/social/listen", { | ||
| data: { query, platform, limit }, | ||
| params: { sync }, | ||
| }); | ||
| return jsonContent(result); | ||
| }); | ||
| server.tool("agentseo_ai_overview_extract", "Extract AI Overview insights for a target keyword.", { | ||
| keyword: z.string().describe("Target keyword"), | ||
| location: z.string().default("United States").describe("Geographic location"), | ||
| location: z | ||
| .string() | ||
| .default("United States") | ||
| .describe("Geographic location"), | ||
| language: z.string().default("en").describe("Language code"), | ||
| target_domain: z | ||
| .string() | ||
| .optional() | ||
| .describe("Optional domain to check against the sampled citation candidates"), | ||
| sync: z.boolean().default(true).describe("Whether to run synchronously"), | ||
| }, async ({ keyword, location, language, sync }) => { | ||
| }, async ({ keyword, location, language, target_domain, sync }) => { | ||
| const result = await callAgentSeoApi("/ai-overview/extract", { | ||
| data: { keyword, location, language }, | ||
| data: { keyword, location, language, target_domain }, | ||
| params: { sync }, | ||
@@ -176,4 +241,13 @@ }); | ||
| queries: z.array(z.string()).min(1).max(10).describe("Queries to check"), | ||
| limit_per_query: z.number().int().min(1).max(20).default(8).describe("Per-query result limit"), | ||
| platform: z.enum(["reddit", "twitter", "all"]).default("all").describe("Platform filter"), | ||
| limit_per_query: z | ||
| .number() | ||
| .int() | ||
| .min(1) | ||
| .max(20) | ||
| .default(8) | ||
| .describe("Per-query result limit"), | ||
| platform: z | ||
| .enum(["reddit", "twitter", "all"]) | ||
| .default("all") | ||
| .describe("Platform filter"), | ||
| sync: z.boolean().default(true).describe("Whether to run synchronously"), | ||
@@ -190,4 +264,15 @@ }, async ({ brand, queries, limit_per_query, platform, sync }) => { | ||
| keyword: z.string().describe("Target keyword"), | ||
| lookback_days: z.number().int().min(3).max(180).default(30).describe("Lookback window"), | ||
| threshold: z.number().min(1).max(20).default(3).describe("Rank drop threshold"), | ||
| lookback_days: z | ||
| .number() | ||
| .int() | ||
| .min(3) | ||
| .max(180) | ||
| .default(30) | ||
| .describe("Lookback window"), | ||
| threshold: z | ||
| .number() | ||
| .min(1) | ||
| .max(20) | ||
| .default(3) | ||
| .describe("Rank drop threshold"), | ||
| sync: z.boolean().default(true).describe("Whether to run synchronously"), | ||
@@ -194,0 +279,0 @@ }, async ({ url, keyword, lookback_days, threshold, sync }) => { |
+2
-2
| { | ||
| "name": "@agentseo/mcp-server", | ||
| "version": "0.1.0", | ||
| "version": "0.1.1", | ||
| "type": "module", | ||
| "license": "MIT", | ||
| "bin": { | ||
| "agentseo-mcp-server": "./dist/index.js" | ||
| "agentseo-mcp-server": "dist/index.js" | ||
| }, | ||
@@ -9,0 +9,0 @@ "scripts": { |
+85
-32
| # @agentseo/mcp-server | ||
| The official Model Context Protocol (MCP) server for AgentSEO. Exposes SEO tools to Claude Desktop and other MCP clients. | ||
| The official Model Context Protocol (MCP) server for AgentSEO. Exposes SEO tools to Claude Desktop, Cursor, and other MCP clients. | ||
| ## Installation | ||
| ## Hosted remote MCP | ||
| This package is part of the AgentSEO monorepo. | ||
| AgentSEO also exposes a hosted remote MCP endpoint: | ||
| 1. Build the server: | ||
| ```bash | ||
| npm run build | ||
| ``` | ||
| `https://www.agentseo.dev/mcp` | ||
| 2. Locate the build artifact: | ||
| `./dist/index.js` | ||
| Use a server-side API key with no allowed domains. | ||
| ## Configuration | ||
| ### Claude Code | ||
| The server runs over stdio and requires the following environment variables: | ||
| ```bash | ||
| claude mcp add --transport http agentseo https://www.agentseo.dev/mcp \ | ||
| --header "Authorization: Bearer sk_live_your_key" \ | ||
| --header "x-project-id: client-alpha" \ | ||
| --header "x-workflow-id: nightly-refresh" | ||
| ``` | ||
| - `AGENTSEO_API_KEY`: Your workspace API key. | ||
| - `AGENTSEO_API_URL`: (Optional) Defaults to `http://localhost:3000/api/v1`. | ||
| ### Cursor | ||
| ### Claude Desktop Example | ||
| Add to `claude_desktop_config.json`: | ||
| ```json | ||
| { | ||
| "mcpServers": { | ||
| "agentseo": { | ||
| "url": "https://www.agentseo.dev/mcp", | ||
| "headers": { | ||
| "Authorization": "Bearer sk_live_your_key", | ||
| "x-project-id": "client-alpha", | ||
| "x-workflow-id": "nightly-refresh" | ||
| } | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
| ## Quickstart (Claude Desktop / Cursor) | ||
| ### 1. Set your API key | ||
| ```bash | ||
| export AGENTSEO_API_KEY="sk_live_your_key" | ||
| ``` | ||
| ### 2. Add AgentSEO MCP server to your MCP config | ||
| For Claude Desktop, add this to `claude_desktop_config.json`. | ||
| For Cursor, add the same block in MCP settings: | ||
| ```json | ||
@@ -31,6 +56,9 @@ { | ||
| "agentseo": { | ||
| "command": "node", | ||
| "args": ["/absolute/path/to/packages/mcp-server/dist/index.js"], | ||
| "command": "npx", | ||
| "args": ["-y", "@agentseo/mcp-server"], | ||
| "env": { | ||
| "AGENTSEO_API_KEY": "sk_live_..." | ||
| "AGENTSEO_API_KEY": "sk_live_your_key", | ||
| "AGENTSEO_API_URL": "https://www.agentseo.dev/api/v1", | ||
| "AGENTSEO_PROJECT_ID": "client-alpha", | ||
| "AGENTSEO_WORKFLOW_ID": "nightly-refresh" | ||
| } | ||
@@ -42,19 +70,44 @@ } | ||
| ### 3. Restart your MCP client | ||
| After restart, ask Claude or Cursor: | ||
| `Run agentseo_local_audit for example.com in New York, NY` | ||
| ## Environment Variables | ||
| - `AGENTSEO_API_KEY` (required): Your workspace API key. | ||
| - `AGENTSEO_API_URL` (optional): Defaults to `http://localhost:3000/api/v1`. | ||
| - For hosted API use `https://www.agentseo.dev/api/v1`. | ||
| - `AGENTSEO_PROJECT_ID` (optional): Default project attribution for all tool calls. | ||
| - `AGENTSEO_WORKFLOW_ID` (optional): Default workflow attribution for all tool calls. | ||
| ## Available Tools | ||
| ### `agentseo_search` | ||
| Perform a Google search. | ||
| - `query` (string): Search term. | ||
| - `limit` (number): Max results (default 10). | ||
| - `include_domains` (string[]): Domain whitelist. | ||
| - `agentseo_search` | ||
| - `agentseo_extract` | ||
| - `agentseo_analyze_serp` | ||
| - `agentseo_local_audit` | ||
| - `agentseo_local_audit_batch` | ||
| - `agentseo_content_gap` | ||
| - `agentseo_web_mentions` (preferred; alias: `agentseo_social_listen`) | ||
| - `agentseo_ai_overview_extract` | ||
| - `agentseo_local_visibility_track` | ||
| - `agentseo_llm_mentions_track` | ||
| - `agentseo_content_decay_detect` | ||
| - `agentseo_keyword_cluster_build` | ||
| - `agentseo_rank_track` | ||
| - `agentseo_rank_history` | ||
| - `agentseo_job_status` | ||
| ### `agentseo_extract` | ||
| Extract content from a URL. | ||
| - `url` (string): Target URL. | ||
| - `include_images` (boolean): Keep image markdown. | ||
| `agentseo_ai_overview_extract` also accepts an optional `target_domain` when you want Claude or another MCP client to check whether your site appears in the sampled candidate set for an AI Overview query. | ||
| ### `agentseo_analyze_serp` | ||
| Detailed SERP analysis. | ||
| - `keyword` (string): Target keyword. | ||
| - `location` (string): "United States", "United Kingdom", etc. | ||
| - `device` (string): "desktop" or "mobile". | ||
| ## Local Run (Without Claude) | ||
| ```bash | ||
| AGENTSEO_API_KEY="sk_live_your_key" npx -y @agentseo/mcp-server | ||
| ``` | ||
| If startup succeeds, you should see: | ||
| `AgentSEO MCP Server running on stdio` |
+365
-269
@@ -11,27 +11,30 @@ #!/usr/bin/env node | ||
| const API_BASE_URL = process.env.AGENTSEO_API_URL || "http://localhost:3000/api/v1"; | ||
| const API_BASE_URL = | ||
| process.env.AGENTSEO_API_URL || "http://localhost:3000/api/v1"; | ||
| const API_KEY = process.env.AGENTSEO_API_KEY; | ||
| const PROJECT_ID = process.env.AGENTSEO_PROJECT_ID; | ||
| const WORKFLOW_ID = process.env.AGENTSEO_WORKFLOW_ID; | ||
| if (!API_KEY) { | ||
| console.error("Error: AGENTSEO_API_KEY environment variable is required."); | ||
| process.exit(1); | ||
| console.error("Error: AGENTSEO_API_KEY environment variable is required."); | ||
| process.exit(1); | ||
| } | ||
| const server = new McpServer({ | ||
| name: "AgentSEO MCP Server", | ||
| version: "0.2.0", | ||
| name: "AgentSEO MCP Server", | ||
| version: "0.2.0", | ||
| }); | ||
| interface AxiosLikeError { | ||
| isAxiosError?: boolean; | ||
| message?: string; | ||
| response?: { | ||
| data?: { | ||
| error?: unknown; | ||
| }; | ||
| isAxiosError?: boolean; | ||
| message?: string; | ||
| response?: { | ||
| data?: { | ||
| error?: unknown; | ||
| }; | ||
| }; | ||
| } | ||
| function isAxiosLikeError(error: unknown): error is AxiosLikeError { | ||
| return typeof error === "object" && error !== null && "isAxiosError" in error; | ||
| return typeof error === "object" && error !== null && "isAxiosError" in error; | ||
| } | ||
@@ -42,315 +45,408 @@ | ||
| async function callAgentSeoApi<T = unknown>( | ||
| endpoint: string, | ||
| options: { | ||
| method?: ApiMethod; | ||
| data?: unknown; | ||
| params?: Record<string, string | number | boolean | undefined>; | ||
| } = {} | ||
| endpoint: string, | ||
| options: { | ||
| method?: ApiMethod; | ||
| data?: unknown; | ||
| params?: Record<string, string | number | boolean | undefined>; | ||
| } = {}, | ||
| ): Promise<T> { | ||
| const method = options.method ?? "POST"; | ||
| try { | ||
| const response = await axios.request({ | ||
| url: `${API_BASE_URL}${endpoint}`, | ||
| method, | ||
| data: options.data, | ||
| params: options.params, | ||
| headers: { | ||
| "x-api-key": API_KEY, | ||
| "Content-Type": "application/json", | ||
| }, | ||
| }); | ||
| return response.data as T; | ||
| } catch (error: unknown) { | ||
| if (isAxiosLikeError(error)) { | ||
| const errorData = error.response?.data?.error || error.message; | ||
| throw new Error(`AgentSEO API Error: ${JSON.stringify(errorData)}`); | ||
| } | ||
| throw error; | ||
| const method = options.method ?? "POST"; | ||
| try { | ||
| const response = await axios.request({ | ||
| url: `${API_BASE_URL}${endpoint}`, | ||
| method, | ||
| data: options.data, | ||
| params: options.params, | ||
| headers: { | ||
| "x-api-key": API_KEY, | ||
| "Content-Type": "application/json", | ||
| ...(PROJECT_ID ? { "x-project-id": PROJECT_ID } : {}), | ||
| ...(WORKFLOW_ID ? { "x-workflow-id": WORKFLOW_ID } : {}), | ||
| }, | ||
| }); | ||
| return response.data as T; | ||
| } catch (error: unknown) { | ||
| if (isAxiosLikeError(error)) { | ||
| const errorData = error.response?.data?.error || error.message; | ||
| throw new Error(`AgentSEO API Error: ${JSON.stringify(errorData)}`); | ||
| } | ||
| throw error; | ||
| } | ||
| } | ||
| function jsonContent(payload: unknown) { | ||
| return { | ||
| content: [{ type: "text" as const, text: JSON.stringify(payload, null, 2) }], | ||
| }; | ||
| return { | ||
| content: [ | ||
| { type: "text" as const, text: JSON.stringify(payload, null, 2) }, | ||
| ], | ||
| }; | ||
| } | ||
| server.tool( | ||
| "agentseo_search", | ||
| "Perform a Google search with optional domain filters.", | ||
| { | ||
| query: z.string().describe("The search query"), | ||
| limit: z.number().int().min(1).max(20).default(10).describe("Number of results"), | ||
| include_domains: z.array(z.string()).optional().describe("Restrict results to these domains"), | ||
| exclude_domains: z.array(z.string()).optional().describe("Exclude results from these domains"), | ||
| sync: z.boolean().default(true).describe("Whether to run synchronously"), | ||
| }, | ||
| async ({ query, limit, include_domains, exclude_domains, sync }) => { | ||
| const result = await callAgentSeoApi("/search", { | ||
| data: { query, limit, include_domains, exclude_domains }, | ||
| params: { sync }, | ||
| }); | ||
| return jsonContent(result); | ||
| } | ||
| "agentseo_search", | ||
| "Perform a Google search with optional domain filters.", | ||
| { | ||
| query: z.string().describe("The search query"), | ||
| limit: z | ||
| .number() | ||
| .int() | ||
| .min(1) | ||
| .max(50) | ||
| .default(10) | ||
| .describe("Number of results"), | ||
| include_domains: z | ||
| .array(z.string()) | ||
| .optional() | ||
| .describe("Restrict results to these domains"), | ||
| exclude_domains: z | ||
| .array(z.string()) | ||
| .optional() | ||
| .describe("Exclude results from these domains"), | ||
| sync: z.boolean().default(true).describe("Whether to run synchronously"), | ||
| }, | ||
| async ({ query, limit, include_domains, exclude_domains, sync }) => { | ||
| const result = await callAgentSeoApi("/search", { | ||
| data: { query, limit, include_domains, exclude_domains }, | ||
| params: { sync }, | ||
| }); | ||
| return jsonContent(result); | ||
| }, | ||
| ); | ||
| server.tool( | ||
| "agentseo_extract", | ||
| "Extract content from a URL and convert to Markdown.", | ||
| { | ||
| url: z.string().url().describe("The URL to scrape"), | ||
| include_images: z.boolean().default(false).describe("Include image links in markdown"), | ||
| }, | ||
| async ({ url, include_images }) => { | ||
| const result = await callAgentSeoApi("/extract", { | ||
| data: { url, format: "markdown", include_images }, | ||
| }); | ||
| const parsed = z.object({ content: z.string() }).passthrough().parse(result); | ||
| return { | ||
| content: [{ type: "text", text: parsed.content }], | ||
| }; | ||
| } | ||
| "agentseo_extract", | ||
| "Extract content from a URL and convert to Markdown.", | ||
| { | ||
| url: z.string().url().describe("The URL to scrape"), | ||
| include_images: z | ||
| .boolean() | ||
| .default(false) | ||
| .describe("Include image links in markdown"), | ||
| }, | ||
| async ({ url, include_images }) => { | ||
| const result = await callAgentSeoApi("/extract", { | ||
| data: { url, format: "markdown", include_images }, | ||
| }); | ||
| const parsed = z | ||
| .object({ content: z.string() }) | ||
| .passthrough() | ||
| .parse(result); | ||
| return { | ||
| content: [{ type: "text", text: parsed.content }], | ||
| }; | ||
| }, | ||
| ); | ||
| server.tool( | ||
| "agentseo_analyze_serp", | ||
| "Deep analysis of Search Engine Results Page (SERP) features and competitors.", | ||
| { | ||
| keyword: z.string().describe("Target keyword"), | ||
| location: z.string().default("United States").describe("Geographic location"), | ||
| device: z.enum(["desktop", "mobile"]).default("desktop").describe("Device type"), | ||
| language: z.string().default("en").describe("Language code"), | ||
| sync: z.boolean().default(true).describe("Whether to run synchronously"), | ||
| }, | ||
| async ({ keyword, location, device, language, sync }) => { | ||
| const result = await callAgentSeoApi("/analyze/serp", { | ||
| data: { keyword, location, device, language }, | ||
| params: { sync }, | ||
| }); | ||
| return jsonContent(result); | ||
| } | ||
| "agentseo_analyze_serp", | ||
| "Deep analysis of Search Engine Results Page (SERP) features and competitors.", | ||
| { | ||
| keyword: z.string().describe("Target keyword"), | ||
| location: z | ||
| .string() | ||
| .default("United States") | ||
| .describe("Geographic location"), | ||
| device: z | ||
| .enum(["desktop", "mobile"]) | ||
| .default("desktop") | ||
| .describe("Device type"), | ||
| language: z.string().default("en").describe("Language code"), | ||
| sync: z.boolean().default(true).describe("Whether to run synchronously"), | ||
| }, | ||
| async ({ keyword, location, device, language, sync }) => { | ||
| const result = await callAgentSeoApi("/analyze/serp", { | ||
| data: { keyword, location, device, language }, | ||
| params: { sync }, | ||
| }); | ||
| return jsonContent(result); | ||
| }, | ||
| ); | ||
| server.tool( | ||
| "agentseo_local_audit", | ||
| "Run a local SEO audit for a domain and location.", | ||
| { | ||
| domain: z.string().describe("Domain or business name"), | ||
| location: z.string().describe("Target location"), | ||
| language: z.string().default("en").describe("Language code"), | ||
| sync: z.boolean().default(true).describe("Whether to run synchronously"), | ||
| }, | ||
| async ({ domain, location, language, sync }) => { | ||
| const result = await callAgentSeoApi("/audit/local", { | ||
| data: { domain, location, language }, | ||
| params: { sync }, | ||
| }); | ||
| return jsonContent(result); | ||
| } | ||
| "agentseo_local_audit", | ||
| "Run a local SEO audit for a domain and location.", | ||
| { | ||
| domain: z.string().describe("Domain or business name"), | ||
| location: z.string().describe("Target location"), | ||
| language: z.string().default("en").describe("Language code"), | ||
| sync: z.boolean().default(true).describe("Whether to run synchronously"), | ||
| }, | ||
| async ({ domain, location, language, sync }) => { | ||
| const result = await callAgentSeoApi("/audit/local", { | ||
| data: { domain, location, language }, | ||
| params: { sync }, | ||
| }); | ||
| return jsonContent(result); | ||
| }, | ||
| ); | ||
| server.tool( | ||
| "agentseo_local_audit_batch", | ||
| "Queue up to 10 local audits in one request.", | ||
| { | ||
| items: z.array(z.object({ | ||
| domain: z.string(), | ||
| location: z.string(), | ||
| language: z.string().optional(), | ||
| })).max(10).describe("Audit items"), | ||
| }, | ||
| async ({ items }) => { | ||
| const result = await callAgentSeoApi("/audit/local/batch", { | ||
| data: { items }, | ||
| }); | ||
| return jsonContent(result); | ||
| } | ||
| "agentseo_local_audit_batch", | ||
| "Queue up to 10 local audits in one request.", | ||
| { | ||
| items: z | ||
| .array( | ||
| z.object({ | ||
| domain: z.string(), | ||
| location: z.string(), | ||
| language: z.string().optional(), | ||
| }), | ||
| ) | ||
| .max(10) | ||
| .describe("Audit items"), | ||
| }, | ||
| async ({ items }) => { | ||
| const result = await callAgentSeoApi("/audit/local/batch", { | ||
| data: { items }, | ||
| }); | ||
| return jsonContent(result); | ||
| }, | ||
| ); | ||
| server.tool( | ||
| "agentseo_content_gap", | ||
| "Run content gap analysis for a URL and keyword.", | ||
| { | ||
| url: z.string().url().describe("Your URL"), | ||
| keyword: z.string().describe("Target keyword"), | ||
| location: z.string().default("United States").describe("Geographic location"), | ||
| language: z.string().default("en").describe("Language code"), | ||
| scrape_depth: z.enum(["h1", "h2", "h3"]).default("h3").describe("How deep to analyze headings"), | ||
| sync: z.boolean().default(true).describe("Whether to run synchronously"), | ||
| }, | ||
| async ({ url, keyword, location, language, scrape_depth, sync }) => { | ||
| const result = await callAgentSeoApi("/content/gap", { | ||
| data: { url, keyword, location, language, scrape_depth }, | ||
| params: { sync }, | ||
| }); | ||
| return jsonContent(result); | ||
| } | ||
| "agentseo_content_gap", | ||
| "Run content gap analysis for a URL and keyword.", | ||
| { | ||
| url: z.string().url().describe("Your URL"), | ||
| keyword: z.string().describe("Target keyword"), | ||
| location: z | ||
| .string() | ||
| .default("United States") | ||
| .describe("Geographic location"), | ||
| language: z.string().default("en").describe("Language code"), | ||
| scrape_depth: z | ||
| .enum(["h1", "h2", "h3"]) | ||
| .default("h3") | ||
| .describe("How deep to analyze headings"), | ||
| sync: z.boolean().default(true).describe("Whether to run synchronously"), | ||
| }, | ||
| async ({ url, keyword, location, language, scrape_depth, sync }) => { | ||
| const result = await callAgentSeoApi("/content/gap", { | ||
| data: { url, keyword, location, language, scrape_depth }, | ||
| params: { sync }, | ||
| }); | ||
| return jsonContent(result); | ||
| }, | ||
| ); | ||
| server.tool( | ||
| "agentseo_social_listen", | ||
| "Run social listening analysis for a query.", | ||
| { | ||
| query: z.string().describe("Search query"), | ||
| platform: z.enum(["reddit", "twitter", "all"]).default("all").describe("Platform filter"), | ||
| limit: z.number().int().min(1).max(20).default(10).describe("Result limit"), | ||
| sync: z.boolean().default(true).describe("Whether to run synchronously"), | ||
| }, | ||
| async ({ query, platform, limit, sync }) => { | ||
| const result = await callAgentSeoApi("/social/listen", { | ||
| data: { query, platform, limit }, | ||
| params: { sync }, | ||
| }); | ||
| return jsonContent(result); | ||
| } | ||
| "agentseo_social_listen", | ||
| "Find web mentions for a query (web-wide discovery; not platform-native monitoring).", | ||
| { | ||
| query: z.string().describe("Search query"), | ||
| platform: z | ||
| .enum(["reddit", "twitter", "all"]) | ||
| .default("all") | ||
| .describe("Platform filter"), | ||
| limit: z.number().int().min(1).max(50).default(10).describe("Result limit"), | ||
| sync: z.boolean().default(true).describe("Whether to run synchronously"), | ||
| }, | ||
| async ({ query, platform, limit, sync }) => { | ||
| const result = await callAgentSeoApi("/social/listen", { | ||
| data: { query, platform, limit }, | ||
| params: { sync }, | ||
| }); | ||
| return jsonContent(result); | ||
| }, | ||
| ); | ||
| // Preferred name; keep agentseo_social_listen for backward compatibility. | ||
| server.tool( | ||
| "agentseo_ai_overview_extract", | ||
| "Extract AI Overview insights for a target keyword.", | ||
| { | ||
| keyword: z.string().describe("Target keyword"), | ||
| location: z.string().default("United States").describe("Geographic location"), | ||
| language: z.string().default("en").describe("Language code"), | ||
| sync: z.boolean().default(true).describe("Whether to run synchronously"), | ||
| }, | ||
| async ({ keyword, location, language, sync }) => { | ||
| const result = await callAgentSeoApi("/ai-overview/extract", { | ||
| data: { keyword, location, language }, | ||
| params: { sync }, | ||
| }); | ||
| return jsonContent(result); | ||
| } | ||
| "agentseo_web_mentions", | ||
| "Find web mentions for a query (web-wide discovery; not platform-native monitoring).", | ||
| { | ||
| query: z.string().describe("Search query"), | ||
| platform: z | ||
| .enum(["reddit", "twitter", "all"]) | ||
| .default("all") | ||
| .describe("Platform filter"), | ||
| limit: z.number().int().min(1).max(50).default(10).describe("Result limit"), | ||
| sync: z.boolean().default(true).describe("Whether to run synchronously"), | ||
| }, | ||
| async ({ query, platform, limit, sync }) => { | ||
| const result = await callAgentSeoApi("/social/listen", { | ||
| data: { query, platform, limit }, | ||
| params: { sync }, | ||
| }); | ||
| return jsonContent(result); | ||
| }, | ||
| ); | ||
| server.tool( | ||
| "agentseo_local_visibility_track", | ||
| "Track local visibility for a domain across keywords and locations.", | ||
| { | ||
| domain: z.string().describe("Domain to track"), | ||
| keywords: z.array(z.string()).min(1).max(5).describe("Keywords"), | ||
| locations: z.array(z.string()).min(1).max(3).describe("Locations"), | ||
| language: z.string().default("en").describe("Language code"), | ||
| sync: z.boolean().default(true).describe("Whether to run synchronously"), | ||
| }, | ||
| async ({ domain, keywords, locations, language, sync }) => { | ||
| const result = await callAgentSeoApi("/local-visibility/track", { | ||
| data: { domain, keywords, locations, language }, | ||
| params: { sync }, | ||
| }); | ||
| return jsonContent(result); | ||
| } | ||
| "agentseo_ai_overview_extract", | ||
| "Extract AI Overview insights for a target keyword.", | ||
| { | ||
| keyword: z.string().describe("Target keyword"), | ||
| location: z | ||
| .string() | ||
| .default("United States") | ||
| .describe("Geographic location"), | ||
| language: z.string().default("en").describe("Language code"), | ||
| target_domain: z | ||
| .string() | ||
| .optional() | ||
| .describe( | ||
| "Optional domain to check against the sampled citation candidates", | ||
| ), | ||
| sync: z.boolean().default(true).describe("Whether to run synchronously"), | ||
| }, | ||
| async ({ keyword, location, language, target_domain, sync }) => { | ||
| const result = await callAgentSeoApi("/ai-overview/extract", { | ||
| data: { keyword, location, language, target_domain }, | ||
| params: { sync }, | ||
| }); | ||
| return jsonContent(result); | ||
| }, | ||
| ); | ||
| server.tool( | ||
| "agentseo_llm_mentions_track", | ||
| "Track brand mentions across LLM/search-style results.", | ||
| { | ||
| brand: z.string().describe("Brand name"), | ||
| queries: z.array(z.string()).min(1).max(10).describe("Queries to check"), | ||
| limit_per_query: z.number().int().min(1).max(20).default(8).describe("Per-query result limit"), | ||
| platform: z.enum(["reddit", "twitter", "all"]).default("all").describe("Platform filter"), | ||
| sync: z.boolean().default(true).describe("Whether to run synchronously"), | ||
| }, | ||
| async ({ brand, queries, limit_per_query, platform, sync }) => { | ||
| const result = await callAgentSeoApi("/llm-mentions/track", { | ||
| data: { brand, queries, limit_per_query, platform }, | ||
| params: { sync }, | ||
| }); | ||
| return jsonContent(result); | ||
| } | ||
| "agentseo_local_visibility_track", | ||
| "Track local visibility for a domain across keywords and locations.", | ||
| { | ||
| domain: z.string().describe("Domain to track"), | ||
| keywords: z.array(z.string()).min(1).max(5).describe("Keywords"), | ||
| locations: z.array(z.string()).min(1).max(3).describe("Locations"), | ||
| language: z.string().default("en").describe("Language code"), | ||
| sync: z.boolean().default(true).describe("Whether to run synchronously"), | ||
| }, | ||
| async ({ domain, keywords, locations, language, sync }) => { | ||
| const result = await callAgentSeoApi("/local-visibility/track", { | ||
| data: { domain, keywords, locations, language }, | ||
| params: { sync }, | ||
| }); | ||
| return jsonContent(result); | ||
| }, | ||
| ); | ||
| server.tool( | ||
| "agentseo_content_decay_detect", | ||
| "Detect potential content decay for a URL and keyword.", | ||
| { | ||
| url: z.string().url().describe("Target URL"), | ||
| keyword: z.string().describe("Target keyword"), | ||
| lookback_days: z.number().int().min(3).max(180).default(30).describe("Lookback window"), | ||
| threshold: z.number().min(1).max(20).default(3).describe("Rank drop threshold"), | ||
| sync: z.boolean().default(true).describe("Whether to run synchronously"), | ||
| }, | ||
| async ({ url, keyword, lookback_days, threshold, sync }) => { | ||
| const result = await callAgentSeoApi("/content-decay/detect", { | ||
| data: { url, keyword, lookback_days, threshold }, | ||
| params: { sync }, | ||
| }); | ||
| return jsonContent(result); | ||
| } | ||
| "agentseo_llm_mentions_track", | ||
| "Track brand mentions across LLM/search-style results.", | ||
| { | ||
| brand: z.string().describe("Brand name"), | ||
| queries: z.array(z.string()).min(1).max(10).describe("Queries to check"), | ||
| limit_per_query: z | ||
| .number() | ||
| .int() | ||
| .min(1) | ||
| .max(20) | ||
| .default(8) | ||
| .describe("Per-query result limit"), | ||
| platform: z | ||
| .enum(["reddit", "twitter", "all"]) | ||
| .default("all") | ||
| .describe("Platform filter"), | ||
| sync: z.boolean().default(true).describe("Whether to run synchronously"), | ||
| }, | ||
| async ({ brand, queries, limit_per_query, platform, sync }) => { | ||
| const result = await callAgentSeoApi("/llm-mentions/track", { | ||
| data: { brand, queries, limit_per_query, platform }, | ||
| params: { sync }, | ||
| }); | ||
| return jsonContent(result); | ||
| }, | ||
| ); | ||
| server.tool( | ||
| "agentseo_keyword_cluster_build", | ||
| "Build keyword clusters from an input keyword list.", | ||
| { | ||
| keywords: z.array(z.string()).min(1).max(200).describe("Keywords"), | ||
| sync: z.boolean().default(true).describe("Whether to run synchronously"), | ||
| }, | ||
| async ({ keywords, sync }) => { | ||
| const result = await callAgentSeoApi("/keyword-cluster/build", { | ||
| data: { keywords }, | ||
| params: { sync }, | ||
| }); | ||
| return jsonContent(result); | ||
| } | ||
| "agentseo_content_decay_detect", | ||
| "Detect potential content decay for a URL and keyword.", | ||
| { | ||
| url: z.string().url().describe("Target URL"), | ||
| keyword: z.string().describe("Target keyword"), | ||
| lookback_days: z | ||
| .number() | ||
| .int() | ||
| .min(3) | ||
| .max(180) | ||
| .default(30) | ||
| .describe("Lookback window"), | ||
| threshold: z | ||
| .number() | ||
| .min(1) | ||
| .max(20) | ||
| .default(3) | ||
| .describe("Rank drop threshold"), | ||
| sync: z.boolean().default(true).describe("Whether to run synchronously"), | ||
| }, | ||
| async ({ url, keyword, lookback_days, threshold, sync }) => { | ||
| const result = await callAgentSeoApi("/content-decay/detect", { | ||
| data: { url, keyword, lookback_days, threshold }, | ||
| params: { sync }, | ||
| }); | ||
| return jsonContent(result); | ||
| }, | ||
| ); | ||
| server.tool( | ||
| "agentseo_rank_track", | ||
| "Queue a rank tracking check for a URL and keyword.", | ||
| { | ||
| keyword: z.string().describe("Keyword to track"), | ||
| url: z.string().url().describe("URL to track"), | ||
| location: z.string().default("United States").describe("Location"), | ||
| sync: z.boolean().default(true).describe("Whether to run synchronously"), | ||
| }, | ||
| async ({ keyword, url, location, sync }) => { | ||
| const result = await callAgentSeoApi("/rank/track", { | ||
| data: { keyword, url, location }, | ||
| params: { sync }, | ||
| }); | ||
| return jsonContent(result); | ||
| } | ||
| "agentseo_keyword_cluster_build", | ||
| "Build keyword clusters from an input keyword list.", | ||
| { | ||
| keywords: z.array(z.string()).min(1).max(200).describe("Keywords"), | ||
| sync: z.boolean().default(true).describe("Whether to run synchronously"), | ||
| }, | ||
| async ({ keywords, sync }) => { | ||
| const result = await callAgentSeoApi("/keyword-cluster/build", { | ||
| data: { keywords }, | ||
| params: { sync }, | ||
| }); | ||
| return jsonContent(result); | ||
| }, | ||
| ); | ||
| server.tool( | ||
| "agentseo_rank_history", | ||
| "Fetch historical rank data for a keyword and URL.", | ||
| { | ||
| keyword: z.string().describe("Keyword"), | ||
| url: z.string().url().describe("URL"), | ||
| }, | ||
| async ({ keyword, url }) => { | ||
| const result = await callAgentSeoApi("/rank/track", { | ||
| method: "GET", | ||
| params: { keyword, url }, | ||
| }); | ||
| return jsonContent(result); | ||
| } | ||
| "agentseo_rank_track", | ||
| "Queue a rank tracking check for a URL and keyword.", | ||
| { | ||
| keyword: z.string().describe("Keyword to track"), | ||
| url: z.string().url().describe("URL to track"), | ||
| location: z.string().default("United States").describe("Location"), | ||
| sync: z.boolean().default(true).describe("Whether to run synchronously"), | ||
| }, | ||
| async ({ keyword, url, location, sync }) => { | ||
| const result = await callAgentSeoApi("/rank/track", { | ||
| data: { keyword, url, location }, | ||
| params: { sync }, | ||
| }); | ||
| return jsonContent(result); | ||
| }, | ||
| ); | ||
| server.tool( | ||
| "agentseo_job_status", | ||
| "Get status/result for a previously queued job.", | ||
| { | ||
| job_id: z.string().describe("Job ID"), | ||
| }, | ||
| async ({ job_id }) => { | ||
| const result = await callAgentSeoApi(`/jobs/${job_id}`, { | ||
| method: "GET", | ||
| }); | ||
| return jsonContent(result); | ||
| } | ||
| "agentseo_rank_history", | ||
| "Fetch historical rank data for a keyword and URL.", | ||
| { | ||
| keyword: z.string().describe("Keyword"), | ||
| url: z.string().url().describe("URL"), | ||
| }, | ||
| async ({ keyword, url }) => { | ||
| const result = await callAgentSeoApi("/rank/track", { | ||
| method: "GET", | ||
| params: { keyword, url }, | ||
| }); | ||
| return jsonContent(result); | ||
| }, | ||
| ); | ||
| server.tool( | ||
| "agentseo_job_status", | ||
| "Get status/result for a previously queued job.", | ||
| { | ||
| job_id: z.string().describe("Job ID"), | ||
| }, | ||
| async ({ job_id }) => { | ||
| const result = await callAgentSeoApi(`/jobs/${job_id}`, { | ||
| method: "GET", | ||
| }); | ||
| return jsonContent(result); | ||
| }, | ||
| ); | ||
| async function main() { | ||
| const transport = new StdioServerTransport(); | ||
| await server.connect(transport); | ||
| console.error("AgentSEO MCP Server running on stdio"); | ||
| const transport = new StdioServerTransport(); | ||
| await server.connect(transport); | ||
| console.error("AgentSEO MCP Server running on stdio"); | ||
| } | ||
| main().catch((error) => { | ||
| console.error("Server error:", error); | ||
| process.exit(1); | ||
| console.error("Server error:", error); | ||
| process.exit(1); | ||
| }); |
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances
30385
15.36%758
31.14%112
89.83%9
80%