This is page 1 of 4. Use http://codebase.md/dataforseo/mcp-server-typescript?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .dockerignore ├── .gitignore ├── Dockerfile ├── field-config.example.json ├── LICENSE ├── package.json ├── README.md ├── scripts │ └── generate-worker-version.cjs ├── src │ ├── core │ │ ├── client │ │ │ └── dataforseo.client.ts │ │ ├── config │ │ │ ├── field-configuration.ts │ │ │ ├── global.tool.ts │ │ │ └── modules.config.ts │ │ ├── modules │ │ │ ├── ai-optimization │ │ │ │ ├── ai-optimization-api-module.ts │ │ │ │ └── tools │ │ │ │ └── keyword-data │ │ │ │ ├── ai-optimization-keyword-data-locations-and-languages.ts │ │ │ │ └── ai-optimization-keyword-data-search-volume.ts │ │ │ ├── backlinks │ │ │ │ ├── backlinks-api.module.ts │ │ │ │ ├── backlinks.prompt.ts │ │ │ │ └── tools │ │ │ │ ├── backlinks-anchor.tool.ts │ │ │ │ ├── backlinks-backlinks.tool.ts │ │ │ │ ├── backlinks-bulk-backlinks.tool.ts │ │ │ │ ├── backlinks-bulk-new-lost-backlinks.tool.ts │ │ │ │ ├── backlinks-bulk-new-lost-referring-domains.tool.ts │ │ │ │ ├── backlinks-bulk-pages-summary.ts │ │ │ │ ├── backlinks-bulk-ranks.tool.ts │ │ │ │ ├── backlinks-bulk-referring-domains.tool.ts │ │ │ │ ├── backlinks-bulk-spam-score.tool.ts │ │ │ │ ├── backlinks-bulk-spam-score.ts │ │ │ │ ├── backlinks-competitors.tool.ts │ │ │ │ ├── backlinks-domain-intersection.tool.ts │ │ │ │ ├── backlinks-domain-pages-summary.tool.ts │ │ │ │ ├── backlinks-domain-pages.tool.ts │ │ │ │ ├── backlinks-filters.tool.ts │ │ │ │ ├── backlinks-page-intersection.tool.ts │ │ │ │ ├── backlinks-referring-domains.tool.ts │ │ │ │ ├── backlinks-referring-networks.tool.ts │ │ │ │ ├── backlinks-summary.tool.ts │ │ │ │ ├── backlinks-timeseries-new-lost-summary.tool.ts │ │ │ │ └── backlinks-timeseries-summary.tool.ts │ │ │ ├── base.module.ts │ │ │ ├── base.tool.ts │ │ │ ├── business-data-api │ │ │ │ ├── business-data-api.module.ts │ │ │ │ └── tools │ │ │ │ └── listings │ │ │ │ ├── business-listings-filters.tool.ts │ │ │ │ └── business-listings-search.tool.ts │ │ │ ├── content-analysis │ │ │ │ ├── content-analysis-api.module.ts │ │ │ │ └── tools │ │ │ │ ├── content-analysis-phrase-trends.ts │ │ │ │ ├── content-analysis-search.tool.ts │ │ │ │ └── content-analysis-summary.ts │ │ │ ├── dataforseo-labs │ │ │ │ ├── dataforseo-labs-api.module.ts │ │ │ │ ├── dataforseo-labs.prompts.ts │ │ │ │ └── tools │ │ │ │ ├── google │ │ │ │ │ ├── competitor-research │ │ │ │ │ │ ├── google-bulk-traffic-estimation.tool.ts │ │ │ │ │ │ ├── google-domain-competitors.tool.ts │ │ │ │ │ │ ├── google-domain-intersection.tool.ts │ │ │ │ │ │ ├── google-domain-rank-overview.tool.ts │ │ │ │ │ │ ├── google-historical-domain-rank-overview.tool.ts │ │ │ │ │ │ ├── google-historical-serp.ts │ │ │ │ │ │ ├── google-page-intersection.tool.ts │ │ │ │ │ │ ├── google-ranked-keywords.tool.ts │ │ │ │ │ │ ├── google-relevant-pages.ts │ │ │ │ │ │ ├── google-serp-competitors.tool.ts │ │ │ │ │ │ └── google-subdomains.ts │ │ │ │ │ ├── keyword-research │ │ │ │ │ │ ├── google-bulk-keyword-difficulty.tool.ts │ │ │ │ │ │ ├── google-historical-keyword-data.tool.ts │ │ │ │ │ │ ├── google-keyword-overview.tool.ts │ │ │ │ │ │ ├── google-keywords-for-site.tool.ts │ │ │ │ │ │ ├── google-keywords-ideas.tool.ts │ │ │ │ │ │ ├── google-keywords-suggestions.tool.ts │ │ │ │ │ │ ├── google-related-keywords.tool.ts │ │ │ │ │ │ └── google-search-intent.tool.ts │ │ │ │ │ └── market-analysis │ │ │ │ │ └── google-top-searches.tool.ts │ │ │ │ └── labs-filters.tool.ts │ │ │ ├── domain-analytics │ │ │ │ ├── domain-analytics-api.module.ts │ │ │ │ └── tools │ │ │ │ ├── technologies │ │ │ │ │ ├── domain-technologies-filters.tool.ts │ │ │ │ │ └── domain-technologies.tool.ts │ │ │ │ └── whois │ │ │ │ ├── whois-filters.tool.ts │ │ │ │ └── whois-overview.tool.ts │ │ │ ├── keywords-data │ │ │ │ ├── keywords-data-api.module.ts │ │ │ │ └── tools │ │ │ │ ├── dataforseo-trends │ │ │ │ │ ├── dataforseo-trends-demography.tool.ts │ │ │ │ │ ├── dataforseo-trends-explore.tool.ts │ │ │ │ │ └── dataforseo-trends-subregion-interests.tool.ts │ │ │ │ ├── google-ads │ │ │ │ │ └── google-ads-search-volume.tool.ts │ │ │ │ └── google-trends │ │ │ │ ├── google-trends-categories.tool.ts │ │ │ │ └── google-trends-explore.tool.ts │ │ │ ├── onpage │ │ │ │ ├── onpage-api.module.ts │ │ │ │ ├── onpage.prompt.ts │ │ │ │ └── tools │ │ │ │ ├── content-parsing.tool.ts │ │ │ │ ├── instant-pages.tool.ts │ │ │ │ └── lighthouse.tool.ts │ │ │ ├── prompt-definition.ts │ │ │ └── serp │ │ │ ├── serp-api.module.ts │ │ │ ├── serp.prompt.ts │ │ │ └── tools │ │ │ ├── serp-organic-live-advanced.tool.ts │ │ │ ├── serp-organic-locations-list.tool.ts │ │ │ ├── serp-youtube-locations-list.tool.ts │ │ │ ├── serp-youtube-organic-live-advanced.tool.ts │ │ │ ├── serp-youtube-video-comments-live-advanced-tool.ts │ │ │ ├── serp-youtube-video-info-live-advanced.tool.ts │ │ │ └── serp-youtube-video-subtitles-live-advanced-tool.ts │ │ └── utils │ │ ├── field-filter.ts │ │ ├── map-array-to-numbered-keys.ts │ │ ├── module-loader.ts │ │ └── version.ts │ ├── main │ │ ├── cli.ts │ │ ├── index-http.ts │ │ ├── index-sse-http.ts │ │ ├── index.ts │ │ ├── init-mcp-server.ts │ │ └── test.ts │ └── worker │ ├── index-worker.ts │ └── worker-configuration.d.ts ├── tsconfig.json ├── tsconfig.worker.json └── wrangler.jsonc ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | build/* 2 | node_modules/* 3 | .wrangler/* 4 | src/worker/version.worker.ts 5 | instruct.txt 6 | .env 7 | src/main/test2.ts 8 | .vscode/launch.json 9 | .vscode/tasks.json 10 | package-lock.json 11 | 12 | ``` -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- ``` 1 | # Node modules 2 | node_modules 3 | npm-debug.log* 4 | 5 | # Build output (will be generated during build) 6 | build 7 | 8 | # Git 9 | .git 10 | .gitignore 11 | 12 | # Documentation 13 | README.md 14 | *.md 15 | 16 | # IDE files 17 | .vscode 18 | .idea 19 | *.swp 20 | *.swo 21 | 22 | # OS files 23 | .DS_Store 24 | Thumbs.db 25 | 26 | # Environment files 27 | .env 28 | .env.local 29 | .env.*.local 30 | 31 | # Logs 32 | logs 33 | *.log 34 | 35 | # Test files 36 | test 37 | tests 38 | *.test.* 39 | *.spec.* 40 | 41 | # Coverage 42 | coverage 43 | .nyc_output 44 | 45 | # Temporary files 46 | .tmp 47 | tmp 48 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # DataForSEO MCP Server 2 | 3 | Model Context Protocol (MCP) server implementation for DataForSEO, enabling AI assistants to interact with selected DataForSEO APIs and obtain SEO data through a standardized interface. 4 | 5 | ## Features 6 | 7 | - **AI_OPTIMIZATION API**: provides data for keyword discovery, conversational optimization, and real-time LLM benchmarking; 8 | - **SERP API**: real-time Search Engine Results Page (SERP) data for Google, Bing, and Yahoo; 9 | - **KEYWORDS_DATA API**: keyword research and clickstream data, including search volume, cost-per-click, and other metrics; 10 | - **ONPAGE API**: allows crawling websites and webpages according to customizable parameters to obtain on-page SEO performance metrics; 11 | - **DATAFORSEO LABS API**: data on keywords, SERPs, and domains based on DataForSEO's in-house databases and proprietary algorithms; 12 | - **BACKLINKS API**: comprehensive backlink analysis including referring domains, anchor text distribution, and link quality metrics; 13 | - **BUSINESS DATA API**: publicly available data on any business entity; 14 | - **DOMAIN ANALYTICS API**: data on website traffic, technologies, and Whois details; 15 | - **CONTENT ANALYSIS API**: robust source of data for brand monitoring, sentiment analysis, and citation management; 16 | 17 | ## Prerequisites 18 | 19 | - Node.js (v14 or higher) 20 | - DataForSEO API credentials (API login and password) 21 | 22 | ## Installation 23 | 24 | 1. Clone the repository: 25 | ```bash 26 | git clone https://github.com/dataforseo/mcp-server-typescript 27 | cd mcp-server-typescript 28 | ``` 29 | 30 | 2. Install dependencies: 31 | ```bash 32 | npm install 33 | ``` 34 | 35 | 3. Set up environment variables: 36 | ```bash 37 | # Required 38 | export DATAFORSEO_USERNAME=your_username 39 | export DATAFORSEO_PASSWORD=your_password 40 | 41 | # Optional: specify which modules to enable (comma-separated) 42 | # If not set, all modules will be enabled 43 | export ENABLED_MODULES="SERP,KEYWORDS_DATA,ONPAGE,DATAFORSEO_LABS,BACKLINKS,BUSINESS_DATA,DOMAIN_ANALYTICS" 44 | 45 | # Optional: specify which prompts in enabled modules are enable too (prompts names, comma-separated) 46 | # If not set, all prompts from enabled modules will be enabled 47 | export ENABLED_PROMPTS="top_3_google_result_domains,top_5_serp_paid_and_organic" 48 | 49 | # Optional: enable full API responses 50 | # If not set or set to false, the server will filter and transform API responses to a more concise format 51 | # If set to true, the server will return the full, unmodified API responses 52 | export DATAFORSEO_FULL_RESPONSE="false" 53 | 54 | # Optional: enable simple filter schema 55 | # If set to true, a simplified version of the filters schema will be used. 56 | # This is required for ChatGPT APIs or other LLMs that cannot handle nested structures. 57 | export DATAFORSEO_SIMPLE_FILTER="false" 58 | ``` 59 | 60 | ## Installation as an NPM Package 61 | 62 | You can install the package globally: 63 | 64 | ```bash 65 | npm install -g dataforseo-mcp-server 66 | ``` 67 | 68 | Or run it directly without installation: 69 | 70 | ```bash 71 | npx dataforseo-mcp-server 72 | ``` 73 | 74 | Remember to set environment variables before running the command: 75 | 76 | ```bash 77 | # Required environment variables 78 | export DATAFORSEO_USERNAME=your_username 79 | export DATAFORSEO_PASSWORD=your_password 80 | 81 | # Run with npx 82 | npx dataforseo-mcp-server 83 | ``` 84 | 85 | ## Building and Running 86 | 87 | Build the project: 88 | ```bash 89 | npm run build 90 | ``` 91 | 92 | Run the server: 93 | ```bash 94 | # Start local server (direct MCP communication) 95 | npx dataforseo-mcp-server 96 | 97 | # Start HTTP server 98 | npx dataforseo-mcp-server http 99 | ``` 100 | 101 | ## HTTP Server Configuration 102 | 103 | The server runs on port 3000 by default and supports both Basic Authentication and environment variable-based authentication. 104 | 105 | To start the HTTP server, run: 106 | ```bash 107 | npm run http 108 | ``` 109 | 110 | ### Authentication Methods 111 | 112 | 1. **Basic Authentication** 113 | - Send requests with Basic Auth header: 114 | ``` 115 | Authorization: Basic <base64-encoded-credentials> 116 | ``` 117 | - Credentials format: `username:password` 118 | 119 | 2. **Environment Variables** 120 | - If no Basic Auth is provided, the server will use credentials from environment variables: 121 | ```bash 122 | export DATAFORSEO_USERNAME=your_username 123 | export DATAFORSEO_PASSWORD=your_password 124 | # Optional 125 | export DATAFORSEO_SIMPLE_FILTER="false" 126 | export DATAFORSEO_FULL_RESPONSE="true" 127 | ``` 128 | 129 | ## Cloudflare Worker Deployment 130 | 131 | The DataForSEO MCP Server can be deployed as a Cloudflare Worker for serverless, edge-distributed access to DataForSEO APIs. 132 | 133 | ### Worker Features 134 | 135 | - **Edge Distribution**: Deploy globally across Cloudflare's edge network 136 | - **Serverless**: No server management required 137 | - **Auto-scaling**: Handles traffic spikes automatically 138 | - **MCP Protocol Support**: Compatible with both Streamable HTTP and SSE transports 139 | - **Environment Variables**: Secure credential management through Cloudflare dashboard 140 | 141 | ### Quick Start 142 | 143 | 1. **Install Wrangler CLI**: 144 | ```bash 145 | npm install -g wrangler 146 | ``` 147 | 148 | 2. **Configure Worker**: 149 | ```bash 150 | # Login to Cloudflare 151 | wrangler login 152 | 153 | # Set environment variables 154 | wrangler secret put DATAFORSEO_USERNAME 155 | wrangler secret put DATAFORSEO_PASSWORD 156 | ``` 157 | 158 | 3. **Deploy Worker**: 159 | ```bash 160 | # Build and deploy 161 | npm run build 162 | wrangler deploy --main build/index-worker.js 163 | ``` 164 | 165 | ### Configuration 166 | 167 | The worker uses the same environment variables as the standard server: 168 | 169 | - `DATAFORSEO_USERNAME`: Your DataForSEO username 170 | - `DATAFORSEO_PASSWORD`: Your DataForSEO password 171 | - `ENABLED_MODULES`: Comma-separated list of modules to enable 172 | - `ENABLED_PROMPTS`: Comma-separated list of prompt names to enable 173 | - `DATAFORSEO_FULL_RESPONSE`: Set to "true" for full API responses 174 | 175 | ### Worker Endpoints 176 | 177 | Once deployed, your worker will be available at `https://your-worker.your-subdomain.workers.dev/` with the following endpoints: 178 | 179 | - **POST /mcp**: Streamable HTTP transport (recommended) 180 | - **GET /sse**: SSE connection establishment (deprecated) 181 | - **POST /messages**: SSE message handling (deprecated) 182 | - **GET /health**: Health check endpoint 183 | - **GET /**: API documentation page 184 | 185 | ### Advanced Configuration 186 | 187 | Edit `wrangler.jsonc` to customize your deployment: 188 | 189 | ```jsonc 190 | { 191 | "name": "dataforseo-mcp-worker", 192 | "main": "build/index-worker.js", 193 | "compatibility_date": "2025-07-10", 194 | "compatibility_flags": ["nodejs_compat"], 195 | "vars": { 196 | "ENABLED_MODULES": "SERP,KEYWORDS_DATA,ONPAGE,DATAFORSEO_LABS", 197 | "ENABLED_PROMPTS":"top_3_google_result_domains,top_5_serp_paid_and_organic" 198 | } 199 | } 200 | ``` 201 | 202 | ### Usage with Claude 203 | 204 | After deployment, configure Claude to use your worker: 205 | 206 | ```json 207 | { 208 | "name": "DataForSEO", 209 | "description": "Access DataForSEO APIs via Cloudflare Worker", 210 | "transport": { 211 | "type": "http", 212 | "baseUrl": "https://your-worker.your-subdomain.workers.dev/mcp" 213 | } 214 | } 215 | ``` 216 | 217 | ## Available Modules 218 | 219 | The following modules are available to be enabled/disabled: 220 | 221 | - `AI_OPTIMIZATION`: provides data for keyword discovery, conversational optimization, and real-time LLM benchmarking; 222 | - `SERP`: real-time SERP data for Google, Bing, and Yahoo; 223 | - `KEYWORDS_DATA`: keyword research and clickstream data; 224 | - `ONPAGE`: crawl websites and webpages to obtain on-page SEO performance metrics; 225 | - `DATAFORSEO_LABS`: data on keywords, SERPs, and domains based on DataForSEO's databases and algorithms; 226 | - `BACKLINKS`: data on inbound links, referring domains and referring pages for any domain, subdomain, or webpage; 227 | - `BUSINESS_DATA`: based on business reviews and business information publicly shared on the following platforms: Google, Trustpilot, Tripadvisor; 228 | - `DOMAIN_ANALYTICS`: helps identify all possible technologies used for building websites and offers Whois data; 229 | - `CONTENT_ANALYSIS`: help you discover citations of the target keyword or brand and analyze the sentiments around it; 230 | 231 | ## Adding New Tools/Modules 232 | 233 | ### Module Structure 234 | 235 | Each module corresponds to a specific DataForSEO API: 236 | - `AI_OPTIMIZATION`: [AI Optimization API](https://docs.dataforseo.com/v3/ai_optimization/overview) 237 | - `SERP` module → [SERP API](https://docs.dataforseo.com/v3/serp/overview) 238 | - `KEYWORDS_DATA` module → [Keywords Data API](https://docs.dataforseo.com/v3/keywords_data/overview) 239 | - `ONPAGE` module → [OnPage API](https://docs.dataforseo.com/v3/on_page/overview) 240 | - `DATAFORSEO_LABS` module → [DataForSEO Labs API](https://docs.dataforseo.com/v3/dataforseo_labs/overview) 241 | - `BACKLINKS`: module → [Backlinks API](https://docs.dataforseo.com/v3/backlinks/overview) 242 | - `BUSINESS_DATA`: module → [Business Data API](https://docs.dataforseo.com/v3/business_data/overview) 243 | - `DOMAIN_ANALYTICS`: module → [Domain Analytics API](https://docs.dataforseo.com/v3/domain_analytics/overview) 244 | - `CONTENT_ANALYSIS`: module → [Content Analysis API](https://docs.dataforseo.com/v3/content_analysis/overview) 245 | 246 | ### Implementation Options 247 | 248 | You can either: 249 | 1. Add a new tool to an existing module 250 | 2. Create a completely new module 251 | 252 | ### Adding a New Tool 253 | 254 | Here's how to add a new tool to any new or pre-existing module: 255 | 256 | ```typescript 257 | // src/code/modules/your-module/tools/your-tool.tool.ts 258 | import { BaseTool } from '../../base.tool'; 259 | import { DataForSEOClient } from '../../../client/dataforseo.client'; 260 | import { z } from 'zod'; 261 | 262 | export class YourTool extends BaseTool { 263 | constructor(private client: DataForSEOClient) { 264 | super(client); 265 | // DataForSEO API returns extensive data with many fields, which can be overwhelming 266 | // for AI agents to process. We select only the most relevant fields to ensure 267 | // efficient and focused responses. 268 | this.fields = [ 269 | 'title', // Example: Include the title field 270 | 'description', // Example: Include the description field 271 | 'url', // Example: Include the URL field 272 | // Add more fields as needed 273 | ]; 274 | } 275 | 276 | getName() { 277 | return 'your-tool-name'; 278 | } 279 | 280 | getDescription() { 281 | return 'Description of what your tool does'; 282 | } 283 | 284 | getParams(): z.ZodRawShape { 285 | return { 286 | // Required parameters 287 | keyword: z.string().describe('The keyword to search for'), 288 | location: z.string().describe('Location in format "City,Region,Country" or just "Country"'), 289 | 290 | // Optional parameters 291 | fields: z.array(z.string()).optional().describe('Specific fields to return in the response. If not specified, all fields will be returned'), 292 | language: z.string().optional().describe('Language code (e.g., "en")'), 293 | }; 294 | } 295 | 296 | async handle(params: any) { 297 | try { 298 | // Make the API call 299 | const response = await this.client.makeRequest({ 300 | endpoint: '/v3/dataforseo_endpoint_path', 301 | method: 'POST', 302 | body: [{ 303 | // Your request parameters 304 | keyword: params.keyword, 305 | location: params.location, 306 | language: params.language, 307 | }], 308 | }); 309 | 310 | // Validate the response for errors 311 | this.validateResponse(response); 312 | 313 | //if the main data array is specified in tasks[0].result[:] field 314 | const result = this.handleDirectResult(response); 315 | //if main data array specified in tasks[0].result[0].items field 316 | const result = this.handleItemsResult(response); 317 | // Format and return the response 318 | return this.formatResponse(result); 319 | } catch (error) { 320 | // Handle and format any errors 321 | return this.formatErrorResponse(error); 322 | } 323 | } 324 | } 325 | ``` 326 | 327 | ### Creating a New Module 328 | 329 | 1. Create a new directory under `src/core/modules/` for your module: 330 | ```bash 331 | mkdir -p src/core/modules/your-module-name 332 | ``` 333 | 334 | 2. Create module files: 335 | ```typescript 336 | // src/core/modules/your-module-name/your-module-name.module.ts 337 | import { BaseModule } from '../base.module'; 338 | import { DataForSEOClient } from '../../client/dataforseo.client'; 339 | import { YourTool } from './tools/your-tool.tool'; 340 | 341 | export class YourModuleNameModule extends BaseModule { 342 | constructor(private client: DataForSEOClient) { 343 | super(); 344 | } 345 | 346 | getTools() { 347 | return { 348 | 'your-tool-name': new YourTool(this.client), 349 | }; 350 | } 351 | } 352 | ``` 353 | 354 | 3. Register your module in `src/core/config/modules.config.ts`: 355 | ```typescript 356 | export const AVAILABLE_MODULES = [ 357 | 'SERP', 358 | 'KEYWORDS_DATA', 359 | 'ONPAGE', 360 | 'DATAFORSEO_LABS', 361 | 'BACKLINKS', 362 | 'BUSINESS_DATA', 363 | 'DOMAIN_ANALYTICS', 364 | 'CONTENT_ANALYSIS', 365 | 'YOUR_MODULE_NAME' // Add your module name here 366 | ] as const; 367 | ``` 368 | 369 | 4. Initialize your module in `src/main/index.ts`: 370 | ```typescript 371 | if (isModuleEnabled('YOUR_MODULE_NAME', enabledModules)) { 372 | modules.push(new YourModuleNameModule(dataForSEOClient)); 373 | } 374 | ``` 375 | 376 | ## Field Configuration 377 | 378 | The MCP server supports field filtering to customize which data fields are returned in API responses. This helps reduce response size and focus on the most relevant data for your use case. 379 | 380 | ### Configuration File Format 381 | 382 | Create a JSON configuration file with the following structure: 383 | 384 | ```json 385 | { 386 | "supported_fields": { 387 | "tool_name": ["field1", "field2", "field3"], 388 | "another_tool": ["field1", "field2"] 389 | } 390 | } 391 | ``` 392 | 393 | ### Using Field Configuration 394 | 395 | Pass the configuration file using the `--configuration` parameter: 396 | 397 | ```bash 398 | # With npm 399 | npm run cli -- http --configuration field-config.json 400 | 401 | # With npx 402 | npx dataforseo-mcp-server http --configuration field-config.json 403 | 404 | # Local mode 405 | npx dataforseo-mcp-server local --configuration field-config.json 406 | ``` 407 | 408 | ### Configuration Behavior 409 | 410 | - **If a tool is configured**: Only the specified fields will be returned in the response 411 | - **If a tool is not configured**: All available fields will be returned (default behavior) 412 | - **If no configuration file is provided**: All tools return all available fields 413 | 414 | ### Example Configuration File 415 | 416 | The repository includes an example configuration file `field-config.example.json` with optimized field selections for common tools: 417 | 418 | ```json 419 | { 420 | "supported_fields": { 421 | "backlinks_backlinks": [ 422 | "id", 423 | "items.anchor", 424 | "items.backlink_spam_score", 425 | "items.dofollow", 426 | "items.domain_from", 427 | "items.domain_from_country", 428 | "items.domain_from_ip", 429 | "items.domain_from_platform_type", 430 | "items.domain_from_rank", 431 | "items.domain_to", 432 | "items.first_seen", 433 | "items.is_broken", 434 | "items.is_new", 435 | "items.item_type", 436 | "items.last_seen", 437 | "items.links_count", 438 | "items.original", 439 | "items.page_from_encoding", 440 | "items.page_from_external_links", 441 | "items.page_from_internal_links", 442 | "items.page_from_language", 443 | "items.page_from_rank", 444 | "items.page_from_size", 445 | "items.page_from_status_code", 446 | "items.page_from_title", 447 | "items.prev_seen", 448 | "items.rank", 449 | "items.ranked_keywords_info.page_from_keywords_count_top_10", 450 | "items.ranked_keywords_info.page_from_keywords_count_top_100", 451 | "items.ranked_keywords_info.page_from_keywords_count_top_3", 452 | "items.semantic_location", 453 | "items.text_post", 454 | "items.text_pre", 455 | "items.tld_from", 456 | "items.type", 457 | "items.url_from", 458 | "items.url_from_https", 459 | "items.url_to", 460 | "items.url_to_https", 461 | "items.url_to_spam_score", 462 | "items.url_to_status_code", 463 | "status_code", 464 | "status_message" 465 | ], 466 | ... 467 | } 468 | } 469 | ``` 470 | 471 | ### Nested Field Support 472 | 473 | The configuration supports nested field paths using dot notation: 474 | 475 | - `"rating.value"` - Access the `value` field within the `rating` object 476 | - `"items.demography.age.keyword"` - Access deeply nested fields 477 | - `"meta.description"` - Access nested object properties 478 | 479 | ### Field Discovery 480 | 481 | To discover available fields for any tool: 482 | 483 | 1. Run the tool without field configuration to see the full response 484 | 2. Identify the fields you need from the API response 485 | 3. Add those field paths to your configuration file 486 | 487 | ### Creating Your Own Configuration 488 | 489 | 1. Copy the example file: 490 | ```bash 491 | cp field-config.example.json my-config.json 492 | ``` 493 | 494 | 2. Modify the field selections based on your needs 495 | 496 | 3. Use your custom configuration: 497 | ```bash 498 | npx dataforseo-mcp-server http --configuration my-config.json 499 | ``` 500 | 501 | ## What endpoints/APIs do you want us to support next? 502 | 503 | We're always looking to expand the capabilities of this MCP server. If you have specific DataForSEO endpoints or APIs you'd like to see supported, please: 504 | 505 | 1. Check the [DataForSEO API Documentation](https://docs.dataforseo.com/v3/) to see what's available 506 | 2. Open an issue in our GitHub repository with: 507 | - The API/endpoint you'd like to see supported; 508 | - A brief description of your use case; 509 | - Describe any specific features you'd like to see implemented. 510 | 511 | Your feedback helps us prioritize which APIs to support next! 512 | 513 | ## Resources 514 | 515 | - [Model Context Protocol Documentation](https://modelcontextprotocol.io/quickstart) 516 | - [DataForSEO API Documentation](https://docs.dataforseo.com/) ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile 1 | FROM node:22-alpine 2 | WORKDIR /app 3 | COPY package*.json ./ 4 | RUN npm ci --ignore-scripts 5 | COPY tsconfig.json ./ 6 | COPY src/ ./src/ 7 | RUN npm run build 8 | EXPOSE 3000 9 | ENV NODE_ENV=production 10 | ENTRYPOINT ["node", "build/main/main/cli.js"] 11 | CMD ["http"] ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "outDir": "./build/main/", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "types": ["node"], 13 | "sourceMap": true 14 | }, 15 | "include": ["src/**/*.ts"], 16 | "exclude": ["node_modules", "src/worker/**/*"] 17 | } 18 | ``` -------------------------------------------------------------------------------- /tsconfig.worker.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "ES2022", 5 | "moduleResolution": "bundler", 6 | "outDir": "./build/worker", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "types": ["@cloudflare/workers-types", "./src/worker/worker-configuration.d.ts"], 13 | "lib": ["ES2022", "WebWorker"] 14 | }, 15 | "include": [ 16 | "src/**/*.ts" 17 | ] 18 | } 19 | ``` -------------------------------------------------------------------------------- /src/core/modules/prompt-definition.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | 3 | export interface PromptDefinition { 4 | name: string; 5 | title: string; 6 | description?: string; 7 | params?: z.ZodRawShape; 8 | handler: (args: any) => Promise<PromptResult>; 9 | } 10 | 11 | export interface PromptResult { 12 | [x: string]: unknown; 13 | description?: string; 14 | messages: PromptResultMessage[]; 15 | _meta?: { [x: string]: unknown; }; 16 | } 17 | 18 | export interface PromptResultMessage { 19 | role: "user" | "assistant"; 20 | content: { 21 | type: "text"; 22 | text: string; 23 | }; 24 | 25 | [x: string]: any; 26 | } ``` -------------------------------------------------------------------------------- /src/core/utils/map-array-to-numbered-keys.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Maps an array of strings to an object with numbered keys 3 | * @param arr Array of strings to map 4 | * @returns Object with numbered keys (1-based index) 5 | * @example 6 | * const arr = ['val_1', 'val_2', 'val_3'] 7 | * const result = mapArrayToNumberedKeys(arr) 8 | * // result = { '1': 'val_1', '2': 'val_2', '3': 'val_3' } 9 | */ 10 | export function mapArrayToNumberedKeys<T extends string>(arr: T[]): Record<string, T> { 11 | return arr.reduce((acc, val, index) => { 12 | acc[String(index + 1)] = val; 13 | return acc; 14 | }, {} as Record<string, T>); 15 | } ``` -------------------------------------------------------------------------------- /src/core/modules/business-data-api/business-data-api.module.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { BaseModule, ToolDefinition } from '../base.module.js'; 2 | import { PromptDefinition } from '../prompt-definition.js'; 3 | import { BusinessDataBusinessListingsSearchTool } from './tools/listings/business-listings-search.tool.js'; 4 | 5 | export class BusinessDataApiModule extends BaseModule { 6 | getTools(): Record<string, ToolDefinition> { 7 | const tools = [ 8 | new BusinessDataBusinessListingsSearchTool(this.dataForSEOClient), 9 | // Add more tools here 10 | ]; 11 | 12 | return tools.reduce((acc, tool) => ({ 13 | ...acc, 14 | [tool.getName()]: { 15 | description: tool.getDescription(), 16 | params: tool.getParams(), 17 | handler: (params: any) => tool.handle(params), 18 | }, 19 | }), {}); 20 | } 21 | 22 | getPrompts(): Record<string, PromptDefinition> { 23 | return {} 24 | } 25 | } ``` -------------------------------------------------------------------------------- /src/core/modules/keywords-data/tools/google-trends/google-trends-categories.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { BaseTool } from '../../../base.tool.js'; 3 | import { DataForSEOClient } from '../../../../client/dataforseo.client.js'; 4 | 5 | export class GoogleTrendsCategoriesTool extends BaseTool { 6 | constructor(dataForSEOClient: DataForSEOClient) { 7 | super(dataForSEOClient); 8 | } 9 | 10 | getName(): string { 11 | return 'keywords_data_google_trends_categories'; 12 | } 13 | 14 | getDescription(): string { 15 | return 'This endpoint will provide you list of Google Trends Categories'; 16 | } 17 | 18 | getParams(): z.ZodRawShape { 19 | return { 20 | 21 | }; 22 | } 23 | 24 | async handle(params: any): Promise<any> { 25 | try { 26 | const response = await this.dataForSEOClient.makeRequest('/v3/keywords_data/google_trends/categories/live', 'GET'); 27 | return this.validateAndFormatResponse(response); 28 | } catch (error) { 29 | return this.formatErrorResponse(error); 30 | } 31 | } 32 | } ``` -------------------------------------------------------------------------------- /src/core/config/global.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { debug } from 'console'; 2 | import { z } from 'zod'; 3 | 4 | export const GlobalToolConfigSchema = z.object({ 5 | simpleFilter: z.boolean().default(false), 6 | fullResponse: z.boolean().default(false), 7 | debug: z.boolean().default(false) 8 | }); 9 | 10 | export type GlobalToolConfig = z.infer<typeof GlobalToolConfigSchema>; 11 | 12 | // Parse config from environment variables 13 | export function parseGlobalToolConfig(): GlobalToolConfig { 14 | const fullResponseEnv = process.env.DATAFORSEO_FULL_RESPONSE as string; 15 | const debugEnv = process.env.DEBUG as string; 16 | const simpleFilterEnv = process.env.DATAFORSEO_SIMPLE_FILTER as string; 17 | const config = { 18 | fullResponse: fullResponseEnv === 'true', 19 | debug: debugEnv === 'true', 20 | simpleFilter: simpleFilterEnv === 'true' 21 | }; 22 | 23 | return GlobalToolConfigSchema.parse(config); 24 | } 25 | 26 | // Export default config 27 | export const defaultGlobalToolConfig = parseGlobalToolConfig(); ``` -------------------------------------------------------------------------------- /scripts/generate-worker-version.cjs: -------------------------------------------------------------------------------- ``` 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | // Read package.json 5 | const packageJsonPath = path.join(__dirname, '../package.json'); 6 | const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); 7 | 8 | // Generate worker version file content 9 | const content = `// Auto-generated from package.json - do not edit manually 10 | // Generated on: ${new Date().toISOString()} 11 | 12 | export const version = "${packageJson.version}"; 13 | export const name = "${packageJson.name}"; 14 | 15 | export default { 16 | version, 17 | name 18 | }; 19 | `; 20 | 21 | // Ensure the worker directory exists 22 | const workerDir = path.join(__dirname, '../src/worker'); 23 | if (!fs.existsSync(workerDir)) { 24 | fs.mkdirSync(workerDir, { recursive: true }); 25 | } 26 | 27 | // Write the version file 28 | const outputPath = path.join(workerDir, 'version.worker.ts'); 29 | fs.writeFileSync(outputPath, content); 30 | 31 | console.log(`✅ Generated worker version file with version ${packageJson.version}`); 32 | console.log(`📁 Output: ${outputPath}`); 33 | ``` -------------------------------------------------------------------------------- /src/core/modules/ai-optimization/ai-optimization-api-module.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { BaseModule, ToolDefinition } from '../base.module.js'; 2 | import { PromptDefinition } from '../prompt-definition.js'; 3 | import { AiOptimizationKeywordDataLocationsAndLanguagesListTool } from './tools/keyword-data/ai-optimization-keyword-data-locations-and-languages.js'; 4 | import { AiOptimizationKeywordDataSearchVolumeTool } from './tools/keyword-data/ai-optimization-keyword-data-search-volume.js' 5 | 6 | export class AiOptimizationApiModule extends BaseModule { 7 | getTools(): Record<string, ToolDefinition> { 8 | const tools = [ 9 | new AiOptimizationKeywordDataLocationsAndLanguagesListTool(this.dataForSEOClient), 10 | new AiOptimizationKeywordDataSearchVolumeTool(this.dataForSEOClient), 11 | ]; 12 | 13 | return tools.reduce((acc, tool) => ({ 14 | ...acc, 15 | [tool.getName()]: { 16 | description: tool.getDescription(), 17 | params: tool.getParams(), 18 | handler: (params: any) => tool.handle(params), 19 | }, 20 | }), {}); 21 | } 22 | 23 | getPrompts(): Record<string, PromptDefinition> { 24 | return {}; 25 | } 26 | } ``` -------------------------------------------------------------------------------- /src/core/modules/content-analysis/content-analysis-api.module.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { BaseModule, ToolDefinition } from '../base.module.js'; 2 | import { PromptDefinition } from '../prompt-definition.js'; 3 | import { ContentAnalysisPhraseTrendsTool } from './tools/content-analysis-phrase-trends.js'; 4 | import { ContentAnalysisSearchTool } from './tools/content-analysis-search.tool.js'; 5 | import { ContentAnalysisSummaryTool } from './tools/content-analysis-summary.js'; 6 | 7 | export class ContentAnalysisApiModule extends BaseModule { 8 | getTools(): Record<string, ToolDefinition> { 9 | const tools = [ 10 | new ContentAnalysisSearchTool(this.dataForSEOClient), 11 | new ContentAnalysisSummaryTool(this.dataForSEOClient), 12 | new ContentAnalysisPhraseTrendsTool(this.dataForSEOClient), 13 | // Add more tools here 14 | ]; 15 | 16 | return tools.reduce((acc, tool) => ({ 17 | ...acc, 18 | [tool.getName()]: { 19 | description: tool.getDescription(), 20 | params: tool.getParams(), 21 | handler: (params: any) => tool.handle(params), 22 | }, 23 | }), {}); 24 | } 25 | 26 | getPrompts(): Record<string, PromptDefinition> { 27 | return {} 28 | } 29 | } ``` -------------------------------------------------------------------------------- /src/core/modules/ai-optimization/tools/keyword-data/ai-optimization-keyword-data-locations-and-languages.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { BaseTool, DataForSEOFullResponse } from '../../../base.tool.js'; 2 | import { DataForSEOClient } from '../../../../client/dataforseo.client.js'; 3 | import { ZodRawShape } from 'zod'; 4 | 5 | export class AiOptimizationKeywordDataLocationsAndLanguagesListTool extends BaseTool { 6 | 7 | constructor(dataForSEOClient: DataForSEOClient) { 8 | super(dataForSEOClient); 9 | } 10 | 11 | protected supportOnlyFullResponse(): boolean { 12 | return true; 13 | } 14 | 15 | getName(): string { 16 | return "ai_optimization_keyword_data_locations_and_languages"; 17 | } 18 | 19 | getDescription(): string { 20 | return "Utility tool for ai_keyword_data_search_volume to get list of availible locations and languages"; 21 | } 22 | 23 | getParams(): ZodRawShape { 24 | return {}; 25 | } 26 | 27 | async handle(params: any): Promise<any> { 28 | try { 29 | 30 | const response = await this.dataForSEOClient.makeRequest(`/v3/ai_optimization/ai_keyword_data/locations_and_languages`, 'GET', null); 31 | return this.validateAndFormatResponse(response); 32 | } catch (error) { 33 | return this.formatErrorResponse(error); 34 | } 35 | } 36 | 37 | } ``` -------------------------------------------------------------------------------- /src/core/modules/domain-analytics/tools/technologies/domain-technologies.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { DataForSEOClient } from '../../../../client/dataforseo.client.js'; 3 | import { BaseTool } from '../../../base.tool.js'; 4 | 5 | export class DomainTechnologiesTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'domain_analytics_technologies_domain_technologies'; 12 | } 13 | 14 | getDescription(): string { 15 | return `Using this endpoint you will get a list of technologies used in a particular domain`; 16 | } 17 | 18 | getParams(): z.ZodRawShape { 19 | return { 20 | target: z.string().describe(`target domain 21 | required field 22 | domain name of the website to analyze 23 | Note: results will be returned for the specified domain only`) 24 | } 25 | } 26 | 27 | async handle(params: any): Promise<any> { 28 | try { 29 | const response = await this.client.makeRequest('/v3/domain_analytics/technologies/domain_technologies/live', 'POST', [{ 30 | target: params.target 31 | }]); 32 | return this.validateAndFormatResponse(response); 33 | } catch (error) { 34 | return this.formatErrorResponse(error); 35 | } 36 | } 37 | } ``` -------------------------------------------------------------------------------- /src/core/modules/domain-analytics/domain-analytics-api.module.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { BaseModule, ToolDefinition } from '../base.module.js'; 2 | import { PromptDefinition } from '../prompt-definition.js'; 3 | import { DomainTechnologiesTool } from './tools/technologies/domain-technologies.tool.js'; 4 | import { DomainTechnologiesFiltersTool } from './tools/technologies/domain-technologies-filters.tool.js'; 5 | import { WhoisFiltersTool } from './tools/whois/whois-filters.tool.js'; 6 | import { WhoisOverviewTool } from './tools/whois/whois-overview.tool.js'; 7 | 8 | export class DomainAnalyticsApiModule extends BaseModule { 9 | getTools(): Record<string, ToolDefinition> { 10 | const tools = [ 11 | new WhoisOverviewTool(this.dataForSEOClient), 12 | new WhoisFiltersTool(this.dataForSEOClient), 13 | new DomainTechnologiesTool(this.dataForSEOClient), 14 | new DomainTechnologiesFiltersTool(this.dataForSEOClient), 15 | // Add more tools here 16 | ]; 17 | 18 | return tools.reduce((acc, tool) => ({ 19 | ...acc, 20 | [tool.getName()]: { 21 | description: tool.getDescription(), 22 | params: tool.getParams(), 23 | handler: (params: any) => tool.handle(params), 24 | }, 25 | }), {}); 26 | } 27 | 28 | getPrompts(): Record<string, PromptDefinition> { 29 | return {}; 30 | } 31 | } ``` -------------------------------------------------------------------------------- /src/core/modules/base.module.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { DataForSEOClient } from '../client/dataforseo.client.js'; 2 | import { z } from 'zod'; 3 | import { PromptDefinition } from './prompt-definition.js'; 4 | 5 | export interface ToolDefinition { 6 | description: string; 7 | params: z.ZodRawShape; 8 | handler: (params: any) => Promise<any>; 9 | } 10 | 11 | export abstract class BaseModule { 12 | protected dataForSEOClient: DataForSEOClient; 13 | 14 | constructor(dataForSEOClient: DataForSEOClient) { 15 | this.dataForSEOClient = dataForSEOClient; 16 | } 17 | 18 | protected formatError(error: unknown): string { 19 | return error instanceof Error ? error.message : 'Unknown error'; 20 | } 21 | 22 | protected formatResponse(data: any): { content: Array<{ type: string; text: string }> } { 23 | return { 24 | content: [ 25 | { 26 | type: "text", 27 | text: JSON.stringify(data, null, 2), 28 | }, 29 | ], 30 | }; 31 | } 32 | 33 | protected formatErrorResponse(error: unknown): { content: Array<{ type: string; text: string }> } { 34 | return { 35 | content: [ 36 | { 37 | type: "text", 38 | text: `Error: ${this.formatError(error)}`, 39 | }, 40 | ], 41 | }; 42 | } 43 | 44 | abstract getTools(): Record<string, ToolDefinition>; 45 | 46 | abstract getPrompts(): Record<string, PromptDefinition>; 47 | } ``` -------------------------------------------------------------------------------- /src/core/config/modules.config.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | 3 | // Define available module names 4 | export const AVAILABLE_MODULES = ['AI_OPTIMIZATION', 'SERP', 'KEYWORDS_DATA', 'ONPAGE', 'DATAFORSEO_LABS', 'BACKLINKS', 'BUSINESS_DATA', 'DOMAIN_ANALYTICS', 'CONTENT_ANALYSIS'] as const; 5 | export type ModuleName = typeof AVAILABLE_MODULES[number]; 6 | 7 | // Schema for validating the ENABLED_MODULES environment variable 8 | export const EnabledModulesSchema = z.any() 9 | .transform((val:string) => { 10 | if (!val) return AVAILABLE_MODULES; // If not set, enable all modules 11 | return val.toString().split(',').map(name => name.trim().toUpperCase() as ModuleName); 12 | }) 13 | .refine((modules) => { 14 | return modules.every(module => AVAILABLE_MODULES.includes(module)); 15 | }, { 16 | message: `Invalid module name. Available modules are: ${AVAILABLE_MODULES.join(', ')}` 17 | }); 18 | 19 | export type EnabledModules = z.infer<typeof EnabledModulesSchema>; 20 | 21 | // Helper function to check if a module is enabled 22 | export function isModuleEnabled(moduleName: ModuleName, enabledModules: EnabledModules): boolean { 23 | return enabledModules.includes(moduleName); 24 | } 25 | 26 | // Default configuration (all modules enabled) 27 | export const defaultEnabledModules: EnabledModules = AVAILABLE_MODULES; ``` -------------------------------------------------------------------------------- /src/core/modules/onpage/onpage-api.module.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { BaseModule, ToolDefinition } from '../base.module.js'; 2 | import { PromptDefinition } from '../prompt-definition.js'; 3 | import { onpagePrompts } from './onpage.prompt.js'; 4 | import { ContentParsingTool } from './tools/content-parsing.tool.js'; 5 | import { InstantPagesTool } from './tools/instant-pages.tool.js'; 6 | import { LighthouseTool } from './tools/lighthouse.tool.js'; 7 | 8 | export class OnPageApiModule extends BaseModule { 9 | getTools(): Record<string, ToolDefinition> { 10 | const tools = [ 11 | new ContentParsingTool(this.dataForSEOClient), 12 | new InstantPagesTool(this.dataForSEOClient), 13 | new LighthouseTool(this.dataForSEOClient), 14 | // Add more tools here 15 | ]; 16 | 17 | return tools.reduce((acc, tool) => ({ 18 | ...acc, 19 | [tool.getName()]: { 20 | description: tool.getDescription(), 21 | params: tool.getParams(), 22 | handler: (params: any) => tool.handle(params), 23 | }, 24 | }), {}); 25 | } 26 | 27 | getPrompts(): Record<string, PromptDefinition> { 28 | return onpagePrompts.reduce((acc, prompt) => ({ 29 | ...acc, 30 | [prompt.name]: { 31 | description: prompt.description, 32 | params: prompt.params, 33 | handler: (params: any) => { 34 | 35 | return prompt.handler(params); 36 | }, 37 | }, 38 | }), {}); 39 | } 40 | } 41 | ``` -------------------------------------------------------------------------------- /src/main/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 4 | import { DataForSEOClient, DataForSEOConfig } from '../core/client/dataforseo.client.js'; 5 | import { EnabledModulesSchema, isModuleEnabled, defaultEnabledModules } from '../core/config/modules.config.js'; 6 | import { BaseModule, ToolDefinition } from '../core/modules/base.module.js'; 7 | import { z } from 'zod'; 8 | import { ModuleLoaderService } from "../core/utils/module-loader.js"; 9 | import { initializeFieldConfiguration } from '../core/config/field-configuration.js'; 10 | import { name, version } from '../core/utils/version.js'; 11 | import { initMcpServer } from "./init-mcp-server.js"; 12 | 13 | // Initialize field configuration if provided 14 | initializeFieldConfiguration(); 15 | console.error('Starting DataForSEO MCP Server...'); 16 | console.error(`Server name: ${name}, version: ${version}`); 17 | 18 | const server = initMcpServer(process.env.DATAFORSEO_USERNAME, process.env.DATAFORSEO_PASSWORD); 19 | 20 | // Start the server 21 | async function main() { 22 | const transport = new StdioServerTransport(); 23 | console.error('Starting server'); 24 | await server.connect(transport); 25 | console.error("DataForSEO MCP Server running on stdio"); 26 | } 27 | 28 | main().catch((error) => { 29 | console.error("Fatal error in main():", error); 30 | process.exit(1); 31 | }); 32 | ``` -------------------------------------------------------------------------------- /src/core/modules/keywords-data/tools/google-ads/google-ads-search-volume.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { BaseTool } from '../../../base.tool.js'; 3 | import { DataForSEOClient } from '../../../../client/dataforseo.client.js'; 4 | 5 | export class GoogleAdsSearchVolumeTool extends BaseTool { 6 | constructor(dataForSEOClient: DataForSEOClient) { 7 | super(dataForSEOClient); 8 | } 9 | 10 | getName(): string { 11 | return 'keywords_data_google_ads_search_volume'; 12 | } 13 | 14 | getDescription(): string { 15 | return 'Get search volume data for keywords from Google Ads'; 16 | } 17 | 18 | getParams(): z.ZodRawShape { 19 | return { 20 | location_name: z.string().nullable().default(null).describe(`full name of the location 21 | optional field 22 | in format "Country" 23 | example: 24 | United Kingdom`), 25 | language_code: z.string().nullable().default(null).describe(`Language two-letter ISO code (e.g., 'en'). 26 | optional field`), 27 | keywords: z.array(z.string()).describe("Array of keywords to get search volume for"), 28 | }; 29 | } 30 | 31 | async handle(params: any): Promise<any> { 32 | try { 33 | const response = await this.dataForSEOClient.makeRequest('/v3/keywords_data/google_ads/search_volume/live', 'POST', [{ 34 | location_name: params.location_name, 35 | language_code: params.language_code, 36 | keywords: params.keywords, 37 | }]); 38 | return this.validateAndFormatResponse(response); 39 | } catch (error) { 40 | return this.formatErrorResponse(error); 41 | } 42 | } 43 | } ``` -------------------------------------------------------------------------------- /src/core/client/dataforseo.client.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { defaultGlobalToolConfig } from '../config/global.tool.js'; 2 | 3 | export class DataForSEOClient { 4 | private config: DataForSEOConfig; 5 | private authHeader: string; 6 | 7 | constructor(config: DataForSEOConfig) { 8 | this.config = config; 9 | if(defaultGlobalToolConfig.debug) { 10 | console.error('DataForSEOClient initialized with config:', config); 11 | } 12 | const token = btoa(`${config.username}:${config.password}`); 13 | this.authHeader = `Basic ${token}`; 14 | } 15 | 16 | async makeRequest<T>(endpoint: string, method: string = 'POST', body?: any, forceFull: boolean = false): Promise<T> { 17 | let url = `${this.config.baseUrl || "https://api.dataforseo.com"}${endpoint}`; 18 | if(!defaultGlobalToolConfig.fullResponse && !forceFull){ 19 | url += '.ai'; 20 | } 21 | // Import version dynamically to avoid circular dependencies 22 | const { version } = await import('../utils/version.js'); 23 | 24 | const headers = { 25 | 'Authorization': this.authHeader, 26 | 'Content-Type': 'application/json', 27 | 'User-Agent': `DataForSEO-MCP-TypeScript-SDK/${version}` 28 | }; 29 | 30 | console.error(`Making request to ${url} with method ${method} and body`, body); 31 | const response = await fetch(url, { 32 | method, 33 | headers, 34 | body: body ? JSON.stringify(body) : undefined, 35 | }); 36 | 37 | if (!response.ok) { 38 | throw new Error(`HTTP error! status: ${response.status}`); 39 | } 40 | 41 | return response.json(); 42 | } 43 | } 44 | 45 | export interface DataForSEOConfig { 46 | username: string; 47 | password: string; 48 | baseUrl?: string; 49 | } ``` -------------------------------------------------------------------------------- /src/core/modules/ai-optimization/tools/keyword-data/ai-optimization-keyword-data-search-volume.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { BaseTool, DataForSEOFullResponse } from '../../../base.tool.js'; 3 | import { DataForSEOClient } from '../../../../client/dataforseo.client.js'; 4 | import { ZodRawShape } from 'zod'; 5 | 6 | export class AiOptimizationKeywordDataSearchVolumeTool extends BaseTool { 7 | 8 | constructor(dataForSEOClient: DataForSEOClient) { 9 | super(dataForSEOClient); 10 | } 11 | 12 | getName(): string { 13 | return "ai_optimization_keyword_data_search_volume"; 14 | } 15 | 16 | getDescription(): string { 17 | return "This endpoint provides search volume data for your target keywords, reflecting their estimated usage in AI LLMs"; 18 | } 19 | 20 | getParams(): z.ZodRawShape { 21 | return { 22 | keywords: z.array(z.string()).describe("Keywords. The maximum number of keywords you can specify: 1000"), 23 | location_name: z.string().default('United States').describe(`full name of the location, example: 'United Kingdom', 'United States'`), 24 | language_code: z.string().describe("Search engine language code (e.g., 'en')"), 25 | }; 26 | } 27 | 28 | async handle(params: any): Promise<any> { 29 | try { 30 | console.error(JSON.stringify(params, null, 2)); 31 | const response = await this.dataForSEOClient.makeRequest(`/v3/ai_optimization/ai_keyword_data/keywords_search_volume/live`, 'POST', [{ 32 | keywords: params.keywords, 33 | location_name: params.location_name, 34 | language_code: params.language_code 35 | }]); 36 | return this.validateAndFormatResponse(response); 37 | } catch (error) { 38 | return this.formatErrorResponse(error); 39 | } 40 | } 41 | 42 | } ``` -------------------------------------------------------------------------------- /src/core/modules/backlinks/tools/backlinks-bulk-spam-score.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { DataForSEOClient } from '../../../client/dataforseo.client.js'; 3 | import { BaseTool } from '../../base.tool.js'; 4 | 5 | export class BacklinksBulkSpamScoreTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'backlinks_bulk_spam_score'; 12 | } 13 | 14 | getDescription(): string { 15 | return `This endpoint will provide you with spam scores of the domains, subdomains, and pages you specified in the targets array. Spam Score is DataForSEO’s proprietary metric that indicates how “spammy” your target is on a scale from 0 to 100`; 16 | } 17 | 18 | getParams(): z.ZodRawShape { 19 | return { 20 | targets: z.array(z.string()).describe(`domains, subdomains or webpages to get rank for 21 | required field 22 | you can set up to 1000 domains, subdomains or webpages 23 | the domain or subdomain should be specified without https:// and www. 24 | the page should be specified with absolute URL (including http:// or https://) 25 | example: 26 | "targets": [ 27 | "forbes.com", 28 | "cnn.com", 29 | "bbc.com", 30 | "yelp.com", 31 | "https://www.apple.com/iphone/", 32 | "https://ahrefs.com/blog/", 33 | "ibm.com", 34 | "https://variety.com/", 35 | "https://stackoverflow.com/", 36 | "www.trustpilot.com" 37 | ]`) 38 | }; 39 | } 40 | 41 | async handle(params: any): Promise<any> { 42 | try { 43 | const response = await this.client.makeRequest('/v3/backlinks/bulk_spam_score/live', 'POST', [{ 44 | targets: params.targets 45 | }]); 46 | return this.validateAndFormatResponse(response); 47 | } catch (error) { 48 | return this.formatErrorResponse(error); 49 | } 50 | } 51 | } ``` -------------------------------------------------------------------------------- /src/core/modules/backlinks/tools/backlinks-bulk-spam-score.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { DataForSEOClient } from '../../../client/dataforseo.client.js'; 3 | import { BaseTool, DataForSEOResponse } from '../../base.tool.js'; 4 | 5 | export class BacklinksBulkSpamScoreTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'backlinks_bulk_spam_score_tool'; 12 | } 13 | 14 | getDescription(): string { 15 | return `This endpoint will provide you with spam scores of the domains, subdomains, and pages you specified in the targets array. Spam Score is DataForSEO’s proprietary metric that indicates how “spammy” your target is on a scale from 0 to 100`; 16 | } 17 | 18 | getParams(): z.ZodRawShape { 19 | return { 20 | targets: z.array(z.string()).describe(`domains, subdomains or webpages to get rank for 21 | required field 22 | you can set up to 1000 domains, subdomains or webpages 23 | the domain or subdomain should be specified without https:// and www. 24 | the page should be specified with absolute URL (including http:// or https://) 25 | example: 26 | "targets": [ 27 | "forbes.com", 28 | "cnn.com", 29 | "bbc.com", 30 | "yelp.com", 31 | "https://www.apple.com/iphone/", 32 | "https://ahrefs.com/blog/", 33 | "ibm.com", 34 | "https://variety.com/", 35 | "https://stackoverflow.com/", 36 | "www.trustpilot.com" 37 | ]`) 38 | }; 39 | } 40 | 41 | async handle(params: any): Promise<any> { 42 | try { 43 | const response = await this.client.makeRequest('/v3/backlinks/bulk_spam_score/live', 'POST', [{ 44 | targets: params.targets 45 | }]); 46 | return this.validateAndFormatResponse(response); 47 | } catch (error) { 48 | return this.formatErrorResponse(error); 49 | } 50 | } 51 | } ``` -------------------------------------------------------------------------------- /src/core/modules/keywords-data/keywords-data-api.module.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { BaseModule, ToolDefinition } from '../base.module.js'; 2 | import { PromptDefinition } from '../prompt-definition.js'; 3 | import { DataForSeoTrendsDemographyTool } from './tools/dataforseo-trends/dataforseo-trends-demography.tool.js'; 4 | import { DataForSeoTrendsExploreTool } from './tools/dataforseo-trends/dataforseo-trends-explore.tool.js'; 5 | import { DataForSeoTrendsSubregionInterestsTool } from './tools/dataforseo-trends/dataforseo-trends-subregion-interests.tool.js'; 6 | import { GoogleAdsSearchVolumeTool } from './tools/google-ads/google-ads-search-volume.tool.js'; 7 | import { GoogleTrendsCategoriesTool } from './tools/google-trends/google-trends-categories.tool.js'; 8 | import { GoogleTrendsExploreTool } from './tools/google-trends/google-trends-explore.tool.js'; 9 | 10 | export class KeywordsDataApiModule extends BaseModule { 11 | getTools(): Record<string, ToolDefinition> { 12 | const tools = [ 13 | new GoogleAdsSearchVolumeTool(this.dataForSEOClient), 14 | 15 | new DataForSeoTrendsDemographyTool(this.dataForSEOClient), 16 | new DataForSeoTrendsSubregionInterestsTool(this.dataForSEOClient), 17 | new DataForSeoTrendsExploreTool(this.dataForSEOClient), 18 | 19 | new GoogleTrendsCategoriesTool(this.dataForSEOClient), 20 | new GoogleTrendsExploreTool(this.dataForSEOClient), 21 | // Add more tools here 22 | ]; 23 | 24 | return tools.reduce((acc, tool) => ({ 25 | ...acc, 26 | [tool.getName()]: { 27 | description: tool.getDescription(), 28 | params: tool.getParams(), 29 | handler: (params: any) => tool.handle(params), 30 | }, 31 | }), {}); 32 | } 33 | 34 | getPrompts(): Record<string, PromptDefinition> { 35 | return {}; 36 | } 37 | } ``` -------------------------------------------------------------------------------- /src/main/cli.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | 3 | import { spawn } from 'child_process'; 4 | import { fileURLToPath } from 'url'; 5 | import { dirname, join } from 'path'; 6 | 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = dirname(__filename); 9 | 10 | const args = process.argv.slice(2); 11 | // Parse command line arguments 12 | const mode = args[0] || 'local'; 13 | const configIndex = args.indexOf('--configuration'); 14 | const configPath = configIndex !== -1 && configIndex + 1 < args.length ? args[configIndex + 1] : null; 15 | const debugLog = args.includes('--debug') || args.includes('-d'); 16 | // Set environment variable for configuration path if provided 17 | if (configPath) { 18 | process.env.FIELD_CONFIG_PATH = configPath; 19 | console.error(`Using field configuration: ${configPath}`); 20 | } 21 | if( debugLog) { 22 | console.error('Debug mode enabled'); 23 | process.env.DEBUG = 'true'; 24 | } 25 | // Prepare arguments to pass to the spawned process (excluding --configuration args) 26 | const argsWithoutMode = args.slice(1); 27 | const childArgs = argsWithoutMode.filter((_, index) => { 28 | return index !== configIndex - 1 && index !== configIndex;}); 29 | 30 | if (mode === 'http') { 31 | const httpServer = join(__dirname, 'index-http.js'); 32 | spawn('node', [httpServer, ...childArgs], { 33 | stdio: 'inherit', 34 | env: { ...process.env } 35 | }); 36 | } else if (mode === 'sse') { 37 | const sseServer = join(__dirname, 'index-sse-http.js'); 38 | spawn('node', [sseServer, ...childArgs], { 39 | stdio: 'inherit', 40 | env: { ...process.env } 41 | }); 42 | } else { 43 | const localServer = join(__dirname, 'index.js'); 44 | spawn('node', [localServer, ...childArgs], { 45 | stdio: 'inherit', 46 | env: { ...process.env } 47 | }); 48 | } ``` -------------------------------------------------------------------------------- /src/core/modules/onpage/tools/lighthouse.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { BaseTool, DataForSEOFullResponse } from '../../base.tool.js'; 3 | import { DataForSEOClient } from '../../../client/dataforseo.client.js'; 4 | import { DataForSEOResponse } from '../../base.tool.js'; 5 | import { defaultGlobalToolConfig } from '../../../config/global.tool.js'; 6 | 7 | export class LighthouseTool extends BaseTool { 8 | constructor(private client: DataForSEOClient) { 9 | super(client); 10 | } 11 | 12 | getName(): string { 13 | return 'on_page_lighthouse'; 14 | } 15 | 16 | getDescription(): string { 17 | return 'The OnPage Lighthouse API is based on Google’s open-source Lighthouse project for measuring the quality of web pages and web apps.'; 18 | } 19 | 20 | getParams(): z.ZodRawShape { 21 | return { 22 | url: z.string().describe("URL of the page to parse"), 23 | enable_javascript: z.boolean().optional().describe("Enable JavaScript rendering"), 24 | custom_js: z.string().optional().describe("Custom JavaScript code to execute"), 25 | custom_user_agent: z.string().optional().describe("Custom User-Agent header"), 26 | accept_language: z.string().optional().describe("Accept-Language header value"), 27 | }; 28 | } 29 | 30 | async handle(params: any): Promise<any> { 31 | try { 32 | const response = await this.dataForSEOClient.makeRequest('/v3/on_page/lighthouse/live/json', 'POST', [{ 33 | url: params.url, 34 | enable_javascript: params.enable_javascript, 35 | custom_js: params.custom_js, 36 | custom_user_agent: params.custom_user_agent, 37 | accept_language: params.accept_language, 38 | }]); 39 | return this.validateAndFormatResponse(response); 40 | } catch (error) { 41 | return this.formatErrorResponse(error); 42 | } 43 | } 44 | } 45 | ``` -------------------------------------------------------------------------------- /src/core/utils/version.ts: -------------------------------------------------------------------------------- ```typescript 1 | // Environment detection and version loading for both Node.js and Workers 2 | let packageVersion = '1.0.0'; // Default version 3 | let packageName = 'dataforseo-mcp-server'; // Default name 4 | 5 | // Type declarations for worker environment globals 6 | declare global { 7 | var __PACKAGE_VERSION__: string | undefined; 8 | var __PACKAGE_NAME__: string | undefined; 9 | } 10 | 11 | // Check if we're in a Node.js environment (has fs module) 12 | const isNodeEnvironment = typeof globalThis !== 'undefined' && 13 | typeof globalThis.process !== 'undefined' && 14 | globalThis.process.versions?.node; 15 | 16 | if (isNodeEnvironment) { 17 | // Node.js environment - read from package.json 18 | try { 19 | const fs = await import('fs'); 20 | const path = await import('path'); 21 | const { fileURLToPath } = await import('url'); 22 | 23 | // Get the directory of the current module 24 | const __filename = fileURLToPath(import.meta.url); 25 | const __dirname = path.dirname(__filename); 26 | 27 | const packageJsonPath = path.resolve(__dirname, '../../../../package.json'); 28 | const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); 29 | packageVersion = packageJson.version || packageVersion; 30 | packageName = packageJson.name || packageName; 31 | } catch (error: unknown) { 32 | const errorMessage = error instanceof Error ? error.message : String(error); 33 | console.warn('Could not read package.json, using default version:', errorMessage); 34 | } 35 | } else { 36 | // Worker environment - use compile-time constants 37 | // These will be replaced by the build process or use defaults 38 | packageVersion = globalThis.__PACKAGE_VERSION__ || packageVersion; 39 | packageName = globalThis.__PACKAGE_NAME__ || packageName; 40 | } 41 | 42 | export const version = packageVersion; 43 | export const name = packageName; 44 | 45 | export default { 46 | version, 47 | name 48 | }; ``` -------------------------------------------------------------------------------- /src/core/modules/dataforseo-labs/tools/google/competitor-research/google-domain-rank-overview.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { DataForSEOClient } from '../../../../../client/dataforseo.client.js'; 3 | import { BaseTool } from '../../../../base.tool.js'; 4 | 5 | export class GoogleDomainRankOverviewTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'dataforseo_labs_google_domain_rank_overview'; 12 | } 13 | 14 | getDescription(): string { 15 | return `This endpoint will provide you with ranking and traffic data from organic and paid search for the specified domain. You will be able to review the domain ranking distribution in SERPs as well as estimated monthly traffic volume for both organic and paid results.`; 16 | } 17 | 18 | getParams(): z.ZodRawShape { 19 | return { 20 | target: z.string().describe(`target domain`), 21 | location_name: z.string().default("United States").describe(`full name of the location 22 | required field 23 | only in format "Country" (not "City" or "Region") 24 | example: 25 | 'United Kingdom', 'United States', 'Canada'`), 26 | language_code: z.string().default("en").describe( 27 | `language code 28 | required field 29 | example: 30 | en`), 31 | ignore_synonyms: z.boolean().default(true).describe( 32 | `ignore highly similar keywords, if set to true, results will be more accurate`) 33 | }; 34 | } 35 | 36 | async handle(params: any): Promise<any> { 37 | try { 38 | const response = await this.client.makeRequest('/v3/dataforseo_labs/google/domain_rank_overview/live', 'POST', [{ 39 | target: params.target, 40 | location_name: params.location_name, 41 | language_code: params.language_code, 42 | ignore_synonyms: params.ignore_synonyms 43 | }]); 44 | return this.validateAndFormatResponse(response); 45 | } catch (error) { 46 | return this.formatErrorResponse(error); 47 | } 48 | } 49 | } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "dataforseo-mcp-server", 3 | "version": "2.7.12", 4 | "main": "build/main/main/index.js", 5 | "type": "module", 6 | "bin": { 7 | "dataforseo-mcp-server": "./build/main/main/cli.js" 8 | }, 9 | "scripts": { 10 | "build": "tsc && node -e \"require('fs').chmodSync('build/main/main/cli.js', '755')\"", 11 | "start": "node build/main/main/index.js", 12 | "dev": "tsc --watch", 13 | "prepare": "npm run build", 14 | "http": "node build/main/main/index-http.js", 15 | "sse": "node build/main/main/index-sse-http.js", 16 | "cli": "node build/main/main/cli.js", 17 | "worker:prebuild": "node scripts/generate-worker-version.cjs && wrangler types ./src/worker/worker-configuration.d.ts", 18 | "worker:build": "npm run worker:prebuild && tsc --project tsconfig.worker.json", 19 | "worker:dev": "npm run worker:build && wrangler dev", 20 | "worker:deploy": "npm run worker:build && wrangler deploy" 21 | }, 22 | "files": [ 23 | "build" 24 | ], 25 | "keywords": [ 26 | "dataforseo", 27 | "mcp", 28 | "modelcontextprotocol", 29 | "api", 30 | "server", 31 | "seo", 32 | "sse", 33 | "streaming" 34 | ], 35 | "author": "dataforseo", 36 | "license": "Apache-2.0", 37 | "description": "A Model Context Protocol (MCP) server for the DataForSEO API, enabling modular and extensible integration of DataForSEO endpoints with support for both HTTP and SSE transports.", 38 | "repository": { 39 | "type": "git", 40 | "url": "git+https://github.com/dataforseo/mcp-server-typescript.git" 41 | }, 42 | "homepage": "https://github.com/dataforseo/mcp-server-typescript#readme", 43 | "engines": { 44 | "node": ">=20.0.0" 45 | }, 46 | "devDependencies": { 47 | "@types/express": "^5.0.1", 48 | "@types/node": "^22.10.0", 49 | "typescript": "^5.7.2" 50 | }, 51 | "dependencies": { 52 | "@modelcontextprotocol/sdk": "^1.4.0", 53 | "agents": "^0.0.101", 54 | "express": "^4.18.2", 55 | "wrangler": "^4.24.0", 56 | "zod": "^3.25.67" 57 | } 58 | } 59 | ``` -------------------------------------------------------------------------------- /src/core/modules/backlinks/tools/backlinks-bulk-backlinks.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { DataForSEOClient } from '../../../client/dataforseo.client.js'; 3 | import { BaseTool } from '../../base.tool.js'; 4 | 5 | export class BacklinksBulkBacklinksTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'backlinks_bulk_backlinks'; 12 | } 13 | 14 | getDescription(): string { 15 | return `This endpoint will provide you with the number of backlinks pointing to domains, subdomains, and pages specified in the targets array. The returned numbers correspond to all live backlinks, that is, total number of referring links with all attributes (e.g., nofollow, noreferrer, ugc, sponsored etc) that were found during the latest check. 16 | Note that if you indicate a domain as a target, you will get results for the root domain (domain with all of its subdomains), e.g. dataforseo.com and app.dataforseo.com`; 17 | } 18 | 19 | getParams(): z.ZodRawShape { 20 | return { 21 | targets: z.array(z.string()).describe(`domains, subdomains or webpages to get rank for 22 | required field 23 | you can set up to 1000 domains, subdomains or webpages 24 | the domain or subdomain should be specified without https:// and www. 25 | the page should be specified with absolute URL (including http:// or https://) 26 | example: 27 | "targets": [ 28 | "forbes.com", 29 | "cnn.com", 30 | "bbc.com", 31 | "yelp.com", 32 | "https://www.apple.com/iphone/", 33 | "https://ahrefs.com/blog/", 34 | "ibm.com", 35 | "https://variety.com/", 36 | "https://stackoverflow.com/", 37 | "www.trustpilot.com" 38 | ]`) 39 | }; 40 | } 41 | 42 | async handle(params: any): Promise<any> { 43 | try { 44 | const response = await this.client.makeRequest('/v3/backlinks/bulk_backlinks/live', 'POST', [{ 45 | targets: params.targets 46 | }]); 47 | return this.validateAndFormatResponse(response); 48 | } catch (error) { 49 | return this.formatErrorResponse(error); 50 | } 51 | } 52 | } ``` -------------------------------------------------------------------------------- /src/core/modules/dataforseo-labs/tools/google/keyword-research/google-search-intent.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { DataForSEOClient } from '../../../../../client/dataforseo.client.js'; 3 | import { BaseTool } from '../../../../base.tool.js'; 4 | 5 | export class GoogleSearchIntentTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'dataforseo_labs_search_intent'; 12 | } 13 | 14 | getDescription(): string { 15 | return `This endpoint will provide you with search intent data for up to 1,000 keywords. For each keyword that you specify when setting a task, the API will return the keyword's search intent and intent probability. Besides the highest probable search intent, the results will also provide you with other likely search intent(s) and their probability. 16 | Based on keyword data and search results data, our system has been trained to detect four types of search intent: informational, navigational, commercial, transactional.`; 17 | } 18 | 19 | getParams(): z.ZodRawShape { 20 | return { 21 | keywords: z.array(z.string()).describe(`target keywords 22 | required field 23 | UTF-8 encoding 24 | maximum number of keywords you can specify in this array: 1000`), 25 | language_code: z.string().default("en").describe( 26 | `language code 27 | required field 28 | Note: this endpoint currently supports the following languages only: 29 | ar, 30 | zh-TW, 31 | cs, 32 | da, 33 | nl, 34 | en, 35 | fi, 36 | fr, 37 | de, 38 | he, 39 | hi, 40 | it, 41 | ja, 42 | ko, 43 | ms, 44 | nb, 45 | pl, 46 | pt, 47 | ro, 48 | ru, 49 | es, 50 | sv, 51 | th, 52 | uk, 53 | vi, 54 | bg, 55 | hr, 56 | sr, 57 | sl, 58 | bs`), 59 | }; 60 | } 61 | 62 | async handle(params: any): Promise<any> { 63 | try { 64 | const response = await this.client.makeRequest('/v3/dataforseo_labs/google/search_intent/live', 'POST', [{ 65 | keywords: params.keywords, 66 | language_code: params.language_code 67 | }]); 68 | return this.validateAndFormatResponse(response); 69 | } catch (error) { 70 | return this.formatErrorResponse(error); 71 | } 72 | } 73 | } ``` -------------------------------------------------------------------------------- /src/core/modules/dataforseo-labs/tools/google/keyword-research/google-bulk-keyword-difficulty.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { DataForSEOClient } from '../../../../../client/dataforseo.client.js'; 3 | import { BaseTool } from '../../../../base.tool.js'; 4 | 5 | export class GoogleBulkKeywordDifficultyTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'dataforseo_labs_bulk_keyword_difficulty'; 12 | } 13 | 14 | getDescription(): string { 15 | return `This endpoint will provide you with the Keyword Difficulty metric for a maximum of 1,000 keywords in one API request. Keyword Difficulty stands for the relative difficulty of ranking in the first top-10 organic results for the related keyword. Keyword Difficulty in DataForSEO API responses indicates the chance of getting in top-10 organic results for a keyword on a logarithmic scale from 0 to 100.`; 16 | } 17 | 18 | getParams(): z.ZodRawShape { 19 | return { 20 | keywords: z.array(z.string()).describe(`target keywords 21 | required field 22 | UTF-8 encoding 23 | maximum number of keywords you can specify in this array: 1000`), 24 | location_name: z.string().default("United States").describe(`full name of the location 25 | required field 26 | only in format "Country" (not "City" or "Region") 27 | example: 28 | 'United Kingdom', 'United States', 'Canada'`), 29 | language_code: z.string().default("en").describe( 30 | `language code 31 | required field 32 | example: 33 | en`), 34 | }; 35 | } 36 | 37 | async handle(params: any): Promise<any> { 38 | try { 39 | const response = await this.client.makeRequest('/v3/dataforseo_labs/google/bulk_keyword_difficulty/live', 'POST', [{ 40 | keywords: params.keywords, 41 | location_name: params.location_name, 42 | language_code: params.language_code 43 | }]); 44 | return this.validateAndFormatResponse(response); 45 | } catch (error) { 46 | return this.formatErrorResponse(error); 47 | } 48 | } 49 | } ``` -------------------------------------------------------------------------------- /src/core/modules/backlinks/tools/backlinks-bulk-referring-domains.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { DataForSEOClient } from '../../../client/dataforseo.client.js'; 3 | import { BaseTool } from '../../base.tool.js'; 4 | 5 | export class BacklinksBulkReferringDomainsTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'backlinks_bulk_referring_domains'; 12 | } 13 | 14 | getDescription(): string { 15 | return `This endpoint will provide you with the number of referring domains pointing to domains, subdomains, and pages specified in the targets array. The returned numbers are based on all live referring domains, that is, total number of domains pointing to the target with any type of backlinks (e.g., nofollow, noreferrer, ugc, sponsored etc) that were found during the latest check. 16 | Note that if you indicate a domain as a target, you will get result for the root domain (domain with all of its subdomains), e.g. dataforseo.com and app.dataforseo.com`; 17 | } 18 | 19 | getParams(): z.ZodRawShape { 20 | return { 21 | targets: z.array(z.string()).describe(`domains, subdomains or webpages to get rank for 22 | required field 23 | you can set up to 1000 domains, subdomains or webpages 24 | the domain or subdomain should be specified without https:// and www. 25 | the page should be specified with absolute URL (including http:// or https://) 26 | example: 27 | "targets": [ 28 | "forbes.com", 29 | "cnn.com", 30 | "bbc.com", 31 | "yelp.com", 32 | "https://www.apple.com/iphone/", 33 | "https://ahrefs.com/blog/", 34 | "ibm.com", 35 | "https://variety.com/", 36 | "https://stackoverflow.com/", 37 | "www.trustpilot.com" 38 | ]`) 39 | }; 40 | } 41 | 42 | async handle(params: any): Promise<any> { 43 | try { 44 | const response = await this.client.makeRequest('/v3/backlinks/bulk_referring_domains/live', 'POST', [{ 45 | targets: params.targets 46 | }]); 47 | return this.validateAndFormatResponse(response); 48 | } catch (error) { 49 | return this.formatErrorResponse(error); 50 | } 51 | } 52 | } ``` -------------------------------------------------------------------------------- /src/core/modules/backlinks/tools/backlinks-bulk-pages-summary.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { DataForSEOClient } from '../../../client/dataforseo.client.js'; 3 | import { BaseTool } from '../../base.tool.js'; 4 | 5 | export class BacklinksBulkPagesSummaryTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'backlinks_bulk_pages_summary'; 12 | } 13 | 14 | getDescription(): string { 15 | return "This endpoint will provide you with a comprehensive overview of backlinks and related data for a bulk of up to 1000 pages, domains, or subdomains. If you indicate a single page as a target, you will get comprehensive summary data on all backlinks for that page."; 16 | } 17 | 18 | getParams(): z.ZodRawShape { 19 | return { 20 | targets: z.array(z.string()).describe(`domains, subdomains or webpages to get summary data for 21 | required field 22 | a domain or a subdomain should be specified without https:// and www. 23 | a page should be specified with absolute URL (including http:// or https://) 24 | you can specify up to 1000 pages, domains, or subdomains in each request. 25 | note that the URLs you set in a single request cannot belong to more than 100 different domains.`), 26 | include_subdomains: z.boolean().optional().describe(`indicates if indirect links to the target will be included in the results 27 | if set to true, the results will include data on indirect links pointing to a page that either redirects to the target, or points to a canonical page 28 | if set to false, indirect links will be ignored`).default(true) 29 | }; 30 | } 31 | 32 | async handle(params: any): Promise<any> { 33 | try { 34 | const response = await this.client.makeRequest('/v3/backlinks/bulk_pages_summary/live', 'POST', [{ 35 | targets: params.targets, 36 | include_subdomains: params.include_subdomains, 37 | }]); 38 | return this.validateAndFormatResponse(response); 39 | } catch (error) { 40 | return this.formatErrorResponse(error); 41 | } 42 | } 43 | } ``` -------------------------------------------------------------------------------- /src/core/modules/onpage/tools/instant-pages.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { DataForSEOClient } from '../../../client/dataforseo.client.js'; 3 | import { BaseTool } from '../../base.tool.js'; 4 | 5 | export class InstantPagesTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'on_page_instant_pages'; 12 | } 13 | 14 | getDescription(): string { 15 | return "Using this function you will get page-specific data with detailed information on how well a particular page is optimized for organic search"; 16 | } 17 | 18 | getParams(): z.ZodRawShape { 19 | return { 20 | url: z.string().describe("URL to analyze"), 21 | enable_javascript: z.boolean().optional().describe("Enable JavaScript rendering"), 22 | custom_js: z.string().optional().describe("Custom JavaScript code to execute"), 23 | custom_user_agent: z.string().optional().describe("Custom User-Agent header"), 24 | accept_language: z.string().optional().describe(`language header for accessing the website 25 | all locale formats are supported (xx, xx-XX, xxx-XX, etc.) 26 | Note: if you do not specify this parameter, some websites may deny access; in this case, pages will be returned with the "type":"broken in the response array`), 27 | }; 28 | } 29 | 30 | async handle(params: { 31 | url: string; 32 | enable_javascript?: boolean; 33 | custom_js?: string; 34 | custom_user_agent?: string; 35 | accept_language?: string; 36 | }): Promise<any> { 37 | try { 38 | const response = await this.dataForSEOClient.makeRequest('/v3/on_page/instant_pages', 'POST', [{ 39 | url: params.url, 40 | enable_javascript: params.enable_javascript, 41 | custom_js: params.custom_js, 42 | custom_user_agent: params.custom_user_agent, 43 | accept_language: params.accept_language, 44 | }]); 45 | return this.validateAndFormatResponse(response); 46 | } catch (error) { 47 | return this.formatErrorResponse(error); 48 | } 49 | } 50 | } ``` -------------------------------------------------------------------------------- /src/core/modules/backlinks/tools/backlinks-summary.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { DataForSEOClient } from '../../../client/dataforseo.client.js'; 3 | import { BaseTool } from '../../base.tool.js'; 4 | 5 | export class BacklinksSummaryTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'backlinks_summary'; 12 | } 13 | 14 | getDescription(): string { 15 | return "This endpoint will provide you with an overview of backlinks data available for a given domain, subdomain, or webpage"; 16 | } 17 | 18 | getParams(): z.ZodRawShape { 19 | return { 20 | target: z.string().describe(`domain, subdomain or webpage to get backlinks for 21 | required field 22 | a domain or a subdomain should be specified without https:// and www. 23 | a page should be specified with absolute URL (including http:// or https://)`), 24 | include_subdomains: z.boolean().optional().describe(`indicates if indirect links to the target will be included in the results 25 | if set to true, the results will include data on indirect links pointing to a page that either redirects to the target, or points to a canonical page 26 | if set to false, indirect links will be ignored`).default(true), 27 | exclude_internal_backlinks: z.boolean().optional().describe(`indicates if internal backlinks from subdomains to the target will be excluded from the results 28 | if set to true, the results will not include data on internal backlinks from subdomains of the same domain as target 29 | if set to false, internal links will be included in the results`).default(true) 30 | }; 31 | } 32 | 33 | async handle(params: any): Promise<any> { 34 | try { 35 | const response = await this.client.makeRequest('/v3/backlinks/summary/live', 'POST', [{ 36 | target: params.target, 37 | include_subdomains: params.include_subdomains, 38 | exclude_internal_backlinks: params.exclude_internal_backlinks 39 | }]); 40 | return this.validateAndFormatResponse(response); 41 | } catch (error) { 42 | return this.formatErrorResponse(error); 43 | } 44 | } 45 | } ``` -------------------------------------------------------------------------------- /src/main/test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Client } from "@modelcontextprotocol/sdk/client/index.js"; 2 | import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; 3 | import readline from "readline/promises"; 4 | 5 | class MCPClient { 6 | private mcp: Client; 7 | private transport: StdioClientTransport | null = null; 8 | 9 | constructor() { 10 | // Initialize Anthropic client and MCP client 11 | this.mcp = new Client({ name: "mcp-client-cli", version: "1.0.0" }); 12 | } 13 | 14 | async connectToServer(serverScriptPath: string) { 15 | /** 16 | * Connect to an MCP server 17 | * 18 | * @param serverScriptPath - Path to the server script (.py or .js) 19 | */ 20 | try { 21 | // Determine script type and appropriate command 22 | const isJs = serverScriptPath.endsWith(".js"); 23 | const isPy = serverScriptPath.endsWith(".py"); 24 | if (!isJs && !isPy) { 25 | throw new Error("Server script must be a .js or .py file"); 26 | } 27 | const command = isPy 28 | ? process.platform === "win32" 29 | ? "python" 30 | : "python3" 31 | : process.execPath; 32 | 33 | // Initialize transport and connect to server 34 | this.transport = new StdioClientTransport({ 35 | command, 36 | args: [serverScriptPath], 37 | }); 38 | this.mcp.connect(this.transport); 39 | 40 | // List available tools 41 | const toolsResult = await this.mcp.listTools(); 42 | console.log(toolsResult); 43 | console.log( 44 | "Connected to server with tools:", 45 | toolsResult.tools.map(({ name }) => name), 46 | ); 47 | } catch (e) { 48 | console.log("Failed to connect to MCP server: ", e); 49 | throw e; 50 | } 51 | } 52 | 53 | async cleanup() { 54 | /** 55 | * Clean up resources 56 | */ 57 | await this.mcp.close(); 58 | } 59 | } 60 | 61 | async function main() { 62 | if (process.argv.length < 3) { 63 | console.log("Usage: node build/test.js <path_to_server_script>"); 64 | return; 65 | } 66 | const mcpClient = new MCPClient(); 67 | try { 68 | await mcpClient.connectToServer(process.argv[2]); 69 | } finally { 70 | await mcpClient.cleanup(); 71 | process.exit(0); 72 | } 73 | } 74 | 75 | main(); 76 | ``` -------------------------------------------------------------------------------- /src/core/modules/dataforseo-labs/tools/google/competitor-research/google-historical-domain-rank-overview.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { DataForSEOClient } from '../../../../../client/dataforseo.client.js'; 3 | import { BaseTool, DataForSEOResponse } from '../../../../base.tool.js'; 4 | 5 | export class GoogleHistoricalDomainRankOverviewTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'dataforseo_labs_google_historical_rank_overview'; 12 | } 13 | 14 | getDescription(): string { 15 | return `This endpoint will provide you with historical data on rankings and traffic of the specified domain, such as domain ranking distribution in SERPs and estimated monthly traffic volume for both organic and paid results`; 16 | } 17 | 18 | getParams(): z.ZodRawShape { 19 | return { 20 | target: z.string().describe(`target domain`), 21 | location_name: z.string().default("United States").describe(`full name of the location 22 | required field 23 | only in format "Country" (not "City" or "Region") 24 | example: 25 | 'United Kingdom', 'United States', 'Canada'`), 26 | language_code: z.string().default("en").describe( 27 | `language code 28 | required field 29 | example: 30 | en`), 31 | ignore_synonyms: z.boolean().default(true).describe( 32 | `ignore highly similar keywords, if set to true, results will be more accurate`), 33 | include_clickstream_data: z.boolean().optional().default(false).describe( 34 | `Include or exclude data from clickstream-based metrics in the result`) 35 | }; 36 | } 37 | 38 | async handle(params: any): Promise<any> { 39 | try { 40 | const response = await this.client.makeRequest('/v3/dataforseo_labs/google/historical_rank_overview/live', 'POST', [{ 41 | target: params.target, 42 | location_name: params.location_name, 43 | language_code: params.language_code, 44 | ignore_synonyms: params.ignore_synonyms, 45 | include_clickstream_data: params.include_clickstream_data 46 | }]); 47 | return this.validateAndFormatResponse(response); 48 | 49 | } catch (error) { 50 | return this.formatErrorResponse(error); 51 | } 52 | } 53 | } ``` -------------------------------------------------------------------------------- /src/core/modules/serp/tools/serp-youtube-video-info-live-advanced.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { BaseTool } from '../../base.tool.js'; 3 | import { DataForSEOClient } from '../../../client/dataforseo.client.js'; 4 | 5 | export class SerpYoutubeVideoInfoLiveAdvancedTool extends BaseTool { 6 | constructor(dataForSEOClient: DataForSEOClient) { 7 | super(dataForSEOClient); 8 | } 9 | 10 | getName(): string { 11 | return 'serp_youtube_video_info_live_advanced'; 12 | } 13 | 14 | getDescription(): string { 15 | return 'provides data on the video you specify'; 16 | } 17 | 18 | getParams(): z.ZodRawShape { 19 | return { 20 | video_id: z.string().describe("ID of the video"), 21 | location_name: z.string().describe(`full name of the location 22 | required field 23 | Location format - hierarchical, comma-separated (from most specific to least) 24 | Can be one of: 25 | 1. Country only: "United States" 26 | 2. Region,Country: "California,United States" 27 | 3. City,Region,Country: "San Francisco,California,United States"`), 28 | language_code: z.string().describe("search engine language code (e.g., 'en')"), 29 | device: z.string().default('desktop').optional().describe(`device type 30 | optional field 31 | can take the values:desktop, mobile 32 | default value: desktop`), 33 | os: z.string().default('windows').optional().describe(`device operating system 34 | optional field 35 | if you specify desktop in the device field, choose from the following values: windows, macos 36 | default value: windows 37 | if you specify mobile in the device field, choose from the following values: android, ios 38 | default value: android`) 39 | }; 40 | } 41 | 42 | async handle(params:any): Promise<any> { 43 | try { 44 | console.error(JSON.stringify(params, null, 2)); 45 | const response = await this.dataForSEOClient.makeRequest(`/v3/serp/youtube/video_info/live/advanced`, 'POST', [{ 46 | video_id: params.video_id, 47 | location_name: params.location_name, 48 | language_code: params.language_code, 49 | device: params.device, 50 | os: params.os, 51 | }]); 52 | return this.validateAndFormatResponse(response); 53 | } catch (error) { 54 | return this.formatErrorResponse(error); 55 | } 56 | } 57 | } ``` -------------------------------------------------------------------------------- /src/core/modules/serp/tools/serp-organic-locations-list.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { BaseTool, DataForSEOFullResponse } from '../../base.tool.js'; 3 | import { DataForSEOClient } from '../../../client/dataforseo.client.js'; 4 | import { DataForSEOResponse } from '../../base.tool.js'; 5 | 6 | export class SerpOrganicLocationsListTool extends BaseTool { 7 | constructor(dataForSEOClient: DataForSEOClient) { 8 | super(dataForSEOClient); 9 | } 10 | 11 | getName(): string { 12 | return 'serp_locations'; 13 | } 14 | 15 | getDescription(): string { 16 | return 'Utility tool for serp_organic_live_advanced to get list of availible locations.'; 17 | } 18 | 19 | getParams(): z.ZodRawShape { 20 | return { 21 | search_engine: z.string().default('google').describe("search engine name, one of: google, yahoo, bing."), 22 | country_iso_code: z.string().describe("ISO 3166-1 alpha-2 country code, for example: US, GB, MT"), 23 | location_type: z.string().optional().describe("Type of location. Possible variants: 'TV Region','Postal Code','Neighborhood','Governorate','National Park','Quarter','Canton','Airport','Okrug','Prefecture','City','Country','Province','Barrio','Sub-District','Congressional District','Municipality District','district','DMA Region','Union Territory','Territory','Colloquial Area','Autonomous Community','Borough','County','State','District','City Region','Commune','Region','Department','Division','Sub-Ward','Municipality','University'"), 24 | location_name: z.string().optional().describe("Name of location or it`s part.") 25 | }; 26 | } 27 | 28 | async handle(params:any): Promise<any> { 29 | try { 30 | 31 | const payload: Record<string, unknown> = { 32 | 'country_iso_code': params.country_iso_code, 33 | }; 34 | 35 | if (params.location_type) { 36 | payload['location_type'] = params.location_type; 37 | } 38 | 39 | if (params.location_name) { 40 | payload['location_name'] = params.location_name; 41 | } 42 | 43 | const response = await this.dataForSEOClient.makeRequest(`/v3/serp/${params.search_engine}/locations`, 'POST', [payload]); 44 | return this.validateAndFormatResponse(response); 45 | } catch (error) { 46 | return this.formatErrorResponse(error); 47 | } 48 | } 49 | } ``` -------------------------------------------------------------------------------- /src/core/modules/serp/tools/serp-youtube-locations-list.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { BaseTool, DataForSEOFullResponse } from '../../base.tool.js'; 3 | import { DataForSEOClient } from '../../../client/dataforseo.client.js'; 4 | import { DataForSEOResponse } from '../../base.tool.js'; 5 | 6 | 7 | export class SerpYoutubeLocationsListTool extends BaseTool { 8 | constructor(dataForSEOClient: DataForSEOClient) { 9 | super(dataForSEOClient); 10 | } 11 | 12 | getName(): string { 13 | return 'serp_youtube_locations'; 14 | } 15 | 16 | getDescription(): string { 17 | return 'Utility tool to get list of available locations for: serp_youtube_organic_live_advanced, serp_youtube_video_info_live_advanced, serp_youtube_video_comments_live_advanced, serp_youtube_video_subtitles_live_advanced.'; 18 | } 19 | 20 | getParams(): z.ZodRawShape { 21 | return { 22 | country_iso_code: z.string().describe("ISO 3166-1 alpha-2 country code, for example: US, GB, MT"), 23 | location_type: z.string().optional().describe("Type of location. Possible variants: 'TV Region','Postal Code','Neighborhood','Governorate','National Park','Quarter','Canton','Airport','Okrug','Prefecture','City','Country','Province','Barrio','Sub-District','Congressional District','Municipality District','district','DMA Region','Union Territory','Territory','Colloquial Area','Autonomous Community','Borough','County','State','District','City Region','Commune','Region','Department','Division','Sub-Ward','Municipality','University'"), 24 | location_name: z.string().optional().describe("Name of location or it`s part.") 25 | }; 26 | } 27 | 28 | async handle(params:any): Promise<any> { 29 | try { 30 | 31 | const payload: Record<string, unknown> = { 32 | 'country_iso_code': params.country_iso_code, 33 | }; 34 | 35 | if (params.location_type) { 36 | payload['location_type'] = params.location_type; 37 | } 38 | 39 | if (params.location_name) { 40 | payload['location_name'] = params.location_name; 41 | } 42 | 43 | const response = await this.dataForSEOClient.makeRequest(`/v3/serp/youtube/locations`, 'POST', [payload]); 44 | return this.validateAndFormatResponse(response); 45 | } catch (error) { 46 | return this.formatErrorResponse(error); 47 | } 48 | } 49 | } ``` -------------------------------------------------------------------------------- /src/core/modules/dataforseo-labs/tools/google/competitor-research/google-bulk-traffic-estimation.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { DataForSEOClient } from '../../../../../client/dataforseo.client.js'; 3 | import { BaseTool } from '../../../../base.tool.js'; 4 | 5 | export class GoogleBulkTrafficEstimationTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'dataforseo_labs_bulk_traffic_estimation'; 12 | } 13 | 14 | getDescription(): string { 15 | return `This endpoint will provide you with estimated monthly traffic volumes for up to 1,000 domains, subdomains, or webpages. Along with organic search traffic estimations, you will also get separate values for paid search, featured snippet, and local pack results.`; 16 | } 17 | 18 | getParams(): z.ZodRawShape { 19 | return { 20 | targets: z.array(z.string()).describe(`target domains, subdomains, and webpages. 21 | you can specify domains, subdomains, and webpages in this field; 22 | domains and subdomains should be specified without https:// and www.; 23 | pages should be specified with absolute URL, including https:// and www.; 24 | you can set up to 1000 domains, subdomains or webpages`), 25 | location_name: z.string().default("United States").describe(`full name of the location 26 | required field 27 | only in format "Country" (not "City" or "Region") 28 | example: 29 | 'United Kingdom', 'United States', 'Canada'`), 30 | language_code: z.string().default("en").describe( 31 | `language code 32 | required field 33 | example: 34 | en`), 35 | ignore_synonyms: z.boolean().default(true).describe( 36 | `ignore highly similar keywords, if set to true, results will be more accurate`), 37 | 38 | }; 39 | } 40 | 41 | async handle(params: any): Promise<any> { 42 | try { 43 | const response = await this.client.makeRequest('/v3/dataforseo_labs/google/bulk_traffic_estimation/live', 'POST', [{ 44 | targets: params.targets, 45 | location_name: params.location_name, 46 | language_code: params.language_code, 47 | item_types: ['organic'], 48 | ignore_synonyms: params.ignore_synonyms 49 | }]); 50 | return this.validateAndFormatResponse(response); 51 | } catch (error) { 52 | return this.formatErrorResponse(error); 53 | } 54 | } 55 | } ``` -------------------------------------------------------------------------------- /src/core/modules/serp/serp-api.module.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { BaseModule, ToolDefinition } from '../base.module.js'; 2 | import { PromptDefinition } from '../prompt-definition.js'; 3 | import { z } from 'zod'; 4 | import { SerpOrganicLiveAdvancedTool } from './tools/serp-organic-live-advanced.tool.js'; 5 | import { SerpOrganicLocationsListTool } from './tools/serp-organic-locations-list.tool.js'; 6 | import { SerpYoutubeOrganicLiveAdvancedTool } from './tools/serp-youtube-organic-live-advanced.tool.js'; 7 | import { SerpYoutubeVideoInfoLiveAdvancedTool } from './tools/serp-youtube-video-info-live-advanced.tool.js'; 8 | import { SerpYoutubeVideoCommentsLiveAdvancedTool } from './tools/serp-youtube-video-comments-live-advanced-tool.js'; 9 | import { SerpYoutubeVideoSubtitlesLiveAdvancedTool } from './tools/serp-youtube-video-subtitles-live-advanced-tool.js'; 10 | import { SerpYoutubeLocationsListTool } from './tools/serp-youtube-locations-list.tool.js'; 11 | import { serpPrompts } from './serp.prompt.js'; 12 | 13 | export class SerpApiModule extends BaseModule { 14 | getTools(): Record<string, ToolDefinition> { 15 | const tools = [ 16 | new SerpOrganicLiveAdvancedTool(this.dataForSEOClient), 17 | new SerpOrganicLocationsListTool(this.dataForSEOClient), 18 | 19 | new SerpYoutubeLocationsListTool(this.dataForSEOClient), 20 | new SerpYoutubeOrganicLiveAdvancedTool(this.dataForSEOClient), 21 | new SerpYoutubeVideoInfoLiveAdvancedTool(this.dataForSEOClient), 22 | new SerpYoutubeVideoCommentsLiveAdvancedTool(this.dataForSEOClient), 23 | new SerpYoutubeVideoSubtitlesLiveAdvancedTool(this.dataForSEOClient), 24 | // Add more tools here 25 | ]; 26 | 27 | return tools.reduce((acc, tool) => ({ 28 | ...acc, 29 | [tool.getName()]: { 30 | description: tool.getDescription(), 31 | params: tool.getParams(), 32 | handler: (params: any) => tool.handle(params), 33 | }, 34 | }), {}); 35 | } 36 | 37 | getPrompts(): Record<string, PromptDefinition> { 38 | return serpPrompts.reduce((acc, prompt) => ({ 39 | ...acc, 40 | [prompt.name]: { 41 | description: prompt.description, 42 | params: prompt.params, 43 | handler: (params: any) => { 44 | 45 | return prompt.handler(params); 46 | }, 47 | }, 48 | }), {}); 49 | } 50 | } ``` -------------------------------------------------------------------------------- /src/core/modules/dataforseo-labs/tools/google/keyword-research/google-historical-keyword-data.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { DataForSEOClient } from '../../../../../client/dataforseo.client.js'; 3 | import { BaseTool, DataForSEOResponse } from '../../../../base.tool.js'; 4 | 5 | export class GoogleHistoricalKeywordDataTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'dataforseo_labs_google_historical_keyword_data'; 12 | } 13 | 14 | getDescription(): string { 15 | return `This endpoint provides Google historical keyword data for specified keywords, including search volume, cost-per-click, competition values for paid search, monthly searches, and search volume trends. You can get historical keyword data since August, 2021, depending on keywords along with location and language combination`; 16 | } 17 | 18 | getParams(): z.ZodRawShape { 19 | return { 20 | keywords: z.array(z.string()).describe(`keywords 21 | required field 22 | The maximum number of keywords you can specify: 700 23 | The maximum number of characters for each keyword: 80 24 | The maximum number of words for each keyword phrase: 10 25 | the specified keywords will be converted to lowercase format, data will be provided in a separate array 26 | note that if some of the keywords specified in this array are omitted in the results you receive, then our database doesn't contain such keywords and cannot return data on them 27 | you will not be charged for the keywords omitted in the results`), 28 | location_name: z.string().default("United States").describe(`full name of the location 29 | required field 30 | only in format "Country" (not "City" or "Region") 31 | example: 32 | 'United Kingdom', 'United States', 'Canada'`), 33 | language_code: z.string().default("en").describe( 34 | `language code 35 | required field 36 | example: 37 | en`) 38 | }; 39 | } 40 | 41 | async handle(params: any): Promise<any> { 42 | try { 43 | const response = await this.client.makeRequest('/v3/dataforseo_labs/google/historical_keyword_data/live', 'POST', [{ 44 | keywords: params.keywords, 45 | location_name: params.location_name, 46 | language_code: params.language_code 47 | }]); 48 | return this.validateAndFormatResponse(response); 49 | } catch (error) { 50 | return this.formatErrorResponse(error); 51 | } 52 | } 53 | } ``` -------------------------------------------------------------------------------- /src/core/modules/backlinks/tools/backlinks-bulk-ranks.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { DataForSEOClient } from '../../../client/dataforseo.client.js'; 3 | import { BaseTool } from '../../base.tool.js'; 4 | 5 | export class BacklinksBulkRanksTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'backlinks_bulk_ranks'; 12 | } 13 | 14 | getDescription(): string { 15 | return "This endpoint will provide you with rank scores of the domains, subdomains, and pages specified in the targets array. The score is based on the number of referring domains pointing to the specified domains, subdomains, or pages. The rank values represent real-time data for the date of the request and range from 0 (no backlinks detected) to 1,000 (highest rank). A similar scoring system is used in Google’s Page Rank algorithm"; 16 | } 17 | 18 | getParams(): z.ZodRawShape { 19 | return { 20 | targets: z.array(z.string()).describe(`domains, subdomains or webpages to get rank for 21 | required field 22 | you can set up to 1000 domains, subdomains or webpages 23 | the domain or subdomain should be specified without https:// and www. 24 | the page should be specified with absolute URL (including http:// or https://) 25 | example: 26 | "targets": [ 27 | "forbes.com", 28 | "cnn.com", 29 | "bbc.com", 30 | "yelp.com", 31 | "https://www.apple.com/iphone/", 32 | "https://ahrefs.com/blog/", 33 | "ibm.com", 34 | "https://variety.com/", 35 | "https://stackoverflow.com/", 36 | "www.trustpilot.com" 37 | ]`), 38 | rank_scale: z.string().optional().describe(`defines the scale used for calculating and displaying the rank, domain_from_rank, and page_from_rank values 39 | optional field 40 | you can use this parameter to choose whether rank values are presented on a 0–100 or 0–1000 scale 41 | possible values: 42 | one_hundred — rank values are displayed on a 0–100 scale 43 | one_thousand — rank values are displayed on a 0–1000 scale`).default('one_thousand') 44 | }; 45 | } 46 | 47 | async handle(params: any): Promise<any> { 48 | try { 49 | const response = await this.client.makeRequest('/v3/backlinks/bulk_ranks/live', 'POST', [{ 50 | targets: params.targets, 51 | rank_scale: params.rank_scale 52 | }]); 53 | return this.validateAndFormatResponse(response); 54 | } catch (error) { 55 | return this.formatErrorResponse(error); 56 | } 57 | } 58 | } ``` -------------------------------------------------------------------------------- /src/main/init-mcp-server.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { DataForSEOClient, DataForSEOConfig } from "../core/client/dataforseo.client.js"; 3 | import { EnabledModulesSchema } from "../core/config/modules.config.js"; 4 | import { ModuleLoaderService } from "../core/utils/module-loader.js"; 5 | import { BaseModule, ToolDefinition } from "../core/modules/base.module.js"; 6 | import { z } from 'zod'; 7 | import { name, version } from '../core/utils/version.js'; 8 | 9 | 10 | export function initMcpServer(username: string | undefined, password: string | undefined): McpServer { 11 | const server = new McpServer({ 12 | name, 13 | version, 14 | }, { capabilities: { logging: {} } }); 15 | 16 | // Initialize DataForSEO client 17 | const dataForSEOConfig: DataForSEOConfig = { 18 | username: username || "", 19 | password: password || "", 20 | }; 21 | 22 | const dataForSEOClient = new DataForSEOClient(dataForSEOConfig); 23 | console.error('DataForSEO client initialized'); 24 | 25 | // Parse enabled modules from environment 26 | const enabledModules = EnabledModulesSchema.parse(process.env.ENABLED_MODULES); 27 | 28 | // Initialize modules 29 | const modules: BaseModule[] = ModuleLoaderService.loadModules(dataForSEOClient, enabledModules); 30 | 31 | const enabledPrompts = process.env.ENABLED_PROMPTS?.split(',').map(name => name.trim()) || []; 32 | 33 | // Register modules 34 | modules.forEach(module => { 35 | 36 | const tools = module.getTools(); 37 | Object.entries(tools).forEach(([name, tool]) => { 38 | const typedTool = tool as ToolDefinition; 39 | const schema = z.object(typedTool.params); 40 | server.tool( 41 | name, 42 | typedTool.description, 43 | schema.shape, 44 | typedTool.handler 45 | ); 46 | }); 47 | 48 | const prompts = module.getPrompts(); 49 | // Filter prompts based on enabledPrompts configuration 50 | const allowedPrompts = enabledPrompts.length === 0 51 | ? prompts 52 | : Object.fromEntries(Object.entries(prompts).filter(([promptName]) => enabledPrompts.includes(promptName))); 53 | 54 | Object.entries(allowedPrompts).forEach(([name, prompt]) => { 55 | server.registerPrompt( 56 | name, 57 | { 58 | description: prompt.description, 59 | argsSchema: prompt.params, 60 | }, 61 | prompt.handler 62 | ); 63 | }); 64 | }); 65 | 66 | 67 | return server; 68 | } ``` -------------------------------------------------------------------------------- /src/core/modules/serp/tools/serp-youtube-organic-live-advanced.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { BaseTool } from '../../base.tool.js'; 3 | import { DataForSEOClient } from '../../../client/dataforseo.client.js'; 4 | 5 | export class SerpYoutubeOrganicLiveAdvancedTool extends BaseTool { 6 | constructor(dataForSEOClient: DataForSEOClient) { 7 | super(dataForSEOClient); 8 | } 9 | 10 | getName(): string { 11 | return 'serp_youtube_organic_live_advanced'; 12 | } 13 | 14 | getDescription(): string { 15 | return 'provides top 20 blocks of youtube search engine results for a keyword'; 16 | } 17 | 18 | getParams(): z.ZodRawShape { 19 | return { 20 | keyword: z.string().describe("Search keyword"), 21 | location_name: z.string().describe(`full name of the location 22 | required field 23 | Location format - hierarchical, comma-separated (from most specific to least) 24 | Can be one of: 25 | 1. Country only: "United States" 26 | 2. Region,Country: "California,United States" 27 | 3. City,Region,Country: "San Francisco,California,United States"`), 28 | language_code: z.string().describe("search engine language code (e.g., 'en')"), 29 | device: z.string().default('desktop').optional().describe(`device type 30 | optional field 31 | can take the values:desktop, mobile 32 | default value: desktop`), 33 | os: z.string().default('windows').optional().describe(`device operating system 34 | optional field 35 | if you specify desktop in the device field, choose from the following values: windows, macos 36 | default value: windows 37 | if you specify mobile in the device field, choose from the following values: android, ios 38 | default value: android`), 39 | block_depth: z.number().default(20).optional().describe(`parsing depth 40 | optional field 41 | number of blocks of results in SERP 42 | max value: 700`) 43 | }; 44 | } 45 | 46 | async handle(params:any): Promise<any> { 47 | try { 48 | console.error(JSON.stringify(params, null, 2)); 49 | const response = await this.dataForSEOClient.makeRequest(`/v3/serp/youtube/organic/live/advanced`, 'POST', [{ 50 | keyword: params.keyword, 51 | location_name: params.location_name, 52 | language_code: params.language_code, 53 | device: params.device, 54 | os: params.os, 55 | block_depth: params.block_depth, 56 | }]); 57 | return this.validateAndFormatResponse(response); 58 | } catch (error) { 59 | return this.formatErrorResponse(error); 60 | } 61 | } 62 | } 63 | 64 | ``` -------------------------------------------------------------------------------- /src/core/modules/dataforseo-labs/tools/google/keyword-research/google-keyword-overview.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { DataForSEOClient } from '../../../../../client/dataforseo.client.js'; 3 | import { BaseTool, DataForSEOResponse } from '../../../../base.tool.js'; 4 | 5 | export class GoogleKeywordOverviewTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'dataforseo_labs_google_keyword_overview'; 12 | } 13 | 14 | getDescription(): string { 15 | return `This endpoint provides Google keyword data for specified keywords. For each keyword, you will receive current cost-per-click, competition values for paid search, search volume, search intent, monthly searches`; 16 | } 17 | 18 | getParams(): z.ZodRawShape { 19 | return { 20 | keywords: z.array(z.string()).describe(`keywords 21 | required field 22 | The maximum number of keywords you can specify: 700 23 | The maximum number of characters for each keyword: 80 24 | The maximum number of words for each keyword phrase: 10 25 | the specified keywords will be converted to lowercase format, data will be provided in a separate array 26 | note that if some of the keywords specified in this array are omitted in the results you receive, then our database doesn't contain such keywords and cannot return data on them 27 | you will not be charged for the keywords omitted in the results`), 28 | location_name: z.string().default("United States").describe(`full name of the location 29 | required field 30 | only in format "Country" (not "City" or "Region") 31 | example: 32 | 'United Kingdom', 'United States', 'Canada'`), 33 | language_code: z.string().default("en").describe( 34 | `language code 35 | required field 36 | example: 37 | en`), 38 | include_clickstream_data: z.boolean().optional().default(false).describe( 39 | `Include or exclude data from clickstream-based metrics in the result`) 40 | }; 41 | } 42 | 43 | async handle(params: any): Promise<any> { 44 | try { 45 | const response = await this.client.makeRequest('/v3/dataforseo_labs/google/keyword_overview/live', 'POST', [{ 46 | keywords: params.keywords, 47 | location_name: params.location_name, 48 | language_code: params.language_code, 49 | include_clickstream_data: params.include_clickstream_data 50 | }]); 51 | return this.validateAndFormatResponse(response); 52 | } catch (error) { 53 | return this.formatErrorResponse(error); 54 | } 55 | } 56 | } ``` -------------------------------------------------------------------------------- /src/core/modules/backlinks/tools/backlinks-bulk-new-lost-referring-domains.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { DataForSEOClient } from '../../../client/dataforseo.client.js'; 3 | import { BaseTool } from '../../base.tool.js'; 4 | 5 | export class BacklinksBulkNewLostReferringDomainsTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'backlinks_bulk_new_lost_referring_domains'; 12 | } 13 | 14 | getDescription(): string { 15 | return `This endpoint will provide you with the number of referring domains pointing to the domains, subdomains and pages specified in the targets array. 16 | Note that if you indicate a domain as a target, you will get result for the root domain (domain with all of its subdomains), e.g. dataforseo.com and app.dataforseo.com`; 17 | } 18 | 19 | getParams(): z.ZodRawShape { 20 | return { 21 | targets: z.array(z.string()).describe(`domains, subdomains or webpages to get rank for 22 | required field 23 | you can set up to 1000 domains, subdomains or webpages 24 | the domain or subdomain should be specified without https:// and www. 25 | the page should be specified with absolute URL (including http:// or https://) 26 | example: 27 | "targets": [ 28 | "forbes.com", 29 | "cnn.com", 30 | "bbc.com", 31 | "yelp.com", 32 | "https://www.apple.com/iphone/", 33 | "https://ahrefs.com/blog/", 34 | "ibm.com", 35 | "https://variety.com/", 36 | "https://stackoverflow.com/", 37 | "www.trustpilot.com" 38 | ]`), 39 | date_from: z.string().optional().describe(`starting date of the time range 40 | optional field 41 | this field indicates the date which will be used as a threshold for new and lost backlinks; 42 | the backlinks that appeared in our index after the specified date will be considered as new; 43 | the backlinks that weren’t found after the specified date, but were present before, will be considered as lost; 44 | default value: today’s date -(minus) one month; 45 | e.g. if today is 2021-10-13, default date_from will be 2021-09-13. 46 | minimum value equals today’s date -(minus) one year; 47 | e.g. if today is 2021-10-13, minimum date_from will be 2020-10-13. 48 | 49 | date format: "yyyy-mm-dd" 50 | example: 51 | "2021-01-01"`) 52 | }; 53 | } 54 | 55 | async handle(params: any): Promise<any> { 56 | try { 57 | const response = await this.client.makeRequest('/v3/backlinks/bulk_new_lost_referring_domains/live', 'POST', [{ 58 | targets: params.targets 59 | }]); 60 | return this.validateAndFormatResponse(response); 61 | } catch (error) { 62 | return this.formatErrorResponse(error); 63 | } 64 | } 65 | } ``` -------------------------------------------------------------------------------- /src/core/modules/serp/tools/serp-youtube-video-comments-live-advanced-tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { BaseTool } from '../../base.tool.js'; 3 | import { DataForSEOClient } from '../../../client/dataforseo.client.js'; 4 | 5 | export class SerpYoutubeVideoCommentsLiveAdvancedTool extends BaseTool { 6 | constructor(dataForSEOClient: DataForSEOClient) { 7 | super(dataForSEOClient); 8 | } 9 | 10 | getName(): string { 11 | return 'serp_youtube_video_comments_live_advanced'; 12 | } 13 | 14 | getDescription(): string { 15 | return 'provides data on the video comments you specify'; 16 | } 17 | 18 | getParams(): z.ZodRawShape { 19 | return { 20 | video_id: z.string().describe("ID of the video"), 21 | location_name: z.string().describe(`full name of the location 22 | required field 23 | Location format - hierarchical, comma-separated (from most specific to least) 24 | Can be one of: 25 | 1. Country only: "United States" 26 | 2. Region,Country: "California,United States" 27 | 3. City,Region,Country: "San Francisco,California,United States"`), 28 | language_code: z.string().describe("search engine language code (e.g., 'en')"), 29 | device: z.string().default('desktop').optional().describe(`device type 30 | optional field 31 | can take the values:desktop, mobile 32 | default value: desktop`), 33 | os: z.string().default('windows').optional().describe(`device operating system 34 | optional field 35 | if you specify desktop in the device field, choose from the following values: windows, macos 36 | default value: windows 37 | if you specify mobile in the device field, choose from the following values: android, ios 38 | default value: android`), 39 | depth: z.number().default(20).optional().describe(`parsing depth, number of results in SERP, max value: 700`) 40 | }; 41 | } 42 | 43 | async handle(params: any): Promise<any> { 44 | try { 45 | console.error(JSON.stringify(params, null, 2)); 46 | const response = await this.dataForSEOClient.makeRequest(`/v3/serp/youtube/video_comments/live/advanced`, 'POST', [{ 47 | video_id: params.video_id, 48 | location_name: params.location_name, 49 | language_code: params.language_code, 50 | device: params.device, 51 | os: params.os, 52 | depth: params.depth, 53 | }]); 54 | return this.validateAndFormatResponse(response); 55 | } catch (error) { 56 | return this.formatErrorResponse(error); 57 | } 58 | } 59 | } ``` -------------------------------------------------------------------------------- /src/core/modules/onpage/tools/content-parsing.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { BaseTool, DataForSEOFullResponse } from '../../base.tool.js'; 3 | import { DataForSEOClient } from '../../../client/dataforseo.client.js'; 4 | import { DataForSEOResponse } from '../../base.tool.js'; 5 | import { defaultGlobalToolConfig } from '../../../config/global.tool.js'; 6 | 7 | export class ContentParsingTool extends BaseTool { 8 | constructor(dataForSEOClient: DataForSEOClient) { 9 | super(dataForSEOClient); 10 | } 11 | 12 | getName(): string { 13 | return 'on_page_content_parsing'; 14 | } 15 | 16 | getDescription(): string { 17 | return 'This endpoint allows parsing the content on any page you specify and will return the structured content of the target page, including link URLs, anchors, headings, and textual content.'; 18 | } 19 | 20 | getParams(): z.ZodRawShape { 21 | return { 22 | url: z.string().describe("URL of the page to parse"), 23 | enable_javascript: z.boolean().optional().describe("Enable JavaScript rendering"), 24 | custom_js: z.string().optional().describe("Custom JavaScript code to execute"), 25 | custom_user_agent: z.string().optional().describe("Custom User-Agent header"), 26 | accept_language: z.string().optional().describe("Accept-Language header value"), 27 | }; 28 | } 29 | 30 | async handle(params: { 31 | url: string; 32 | enable_javascript?: boolean; 33 | custom_js?: string; 34 | custom_user_agent?: string; 35 | accept_language?: string; 36 | }): Promise<any> { 37 | try { 38 | const response = await this.dataForSEOClient.makeRequest('/v3/on_page/content_parsing/live', 'POST', [{ 39 | url: params.url, 40 | enable_javascript: params.enable_javascript, 41 | custom_js: params.custom_js, 42 | custom_user_agent: params.custom_user_agent, 43 | accept_language: params.accept_language, 44 | markdown_view: true 45 | }]); 46 | console.error(JSON.stringify(response)); 47 | if(defaultGlobalToolConfig.fullResponse || this.supportOnlyFullResponse()){ 48 | let data = response as DataForSEOFullResponse; 49 | this.validateResponseFull(data); 50 | let result = data.tasks[0].result; 51 | return this.formatResponse(result); 52 | } 53 | else{ 54 | let data = response as DataForSEOResponse; 55 | this.validateResponse(data); 56 | let result = data.items[0].page_as_markdown; 57 | return this.formatResponse(result); 58 | } 59 | } catch (error) { 60 | return this.formatErrorResponse(error); 61 | } 62 | } 63 | } ``` -------------------------------------------------------------------------------- /src/core/utils/module-loader.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { DataForSEOClient } from '../client/dataforseo.client.js'; 2 | import { SerpApiModule } from '../modules/serp/serp-api.module.js'; 3 | import { AiOptimizationApiModule } from '../modules/ai-optimization/ai-optimization-api-module.js' 4 | import { KeywordsDataApiModule } from '../modules/keywords-data/keywords-data-api.module.js'; 5 | import { OnPageApiModule } from '../modules/onpage/onpage-api.module.js'; 6 | import { DataForSEOLabsApi } from '../modules/dataforseo-labs/dataforseo-labs-api.module.js'; 7 | import { BacklinksApiModule } from '../modules/backlinks/backlinks-api.module.js'; 8 | import { BusinessDataApiModule } from '../modules/business-data-api/business-data-api.module.js'; 9 | import { DomainAnalyticsApiModule } from '../modules/domain-analytics/domain-analytics-api.module.js'; 10 | import { BaseModule } from '../modules/base.module.js'; 11 | import { EnabledModules, isModuleEnabled } from '../config/modules.config.js'; 12 | import { ContentAnalysisApiModule } from '../modules/content-analysis/content-analysis-api.module.js'; 13 | 14 | export class ModuleLoaderService { 15 | static loadModules(dataForSEOClient: DataForSEOClient, enabledModules: EnabledModules): BaseModule[] { 16 | const modules: BaseModule[] = []; 17 | 18 | if (isModuleEnabled('AI_OPTIMIZATION', enabledModules)) { 19 | modules.push(new AiOptimizationApiModule(dataForSEOClient)); 20 | } 21 | if (isModuleEnabled('SERP', enabledModules)) { 22 | modules.push(new SerpApiModule(dataForSEOClient)); 23 | } 24 | if (isModuleEnabled('KEYWORDS_DATA', enabledModules)) { 25 | modules.push(new KeywordsDataApiModule(dataForSEOClient)); 26 | } 27 | if (isModuleEnabled('ONPAGE', enabledModules)) { 28 | modules.push(new OnPageApiModule(dataForSEOClient)); 29 | } 30 | if (isModuleEnabled('DATAFORSEO_LABS', enabledModules)) { 31 | modules.push(new DataForSEOLabsApi(dataForSEOClient)); 32 | } 33 | if (isModuleEnabled('BACKLINKS', enabledModules)) { 34 | modules.push(new BacklinksApiModule(dataForSEOClient)); 35 | } 36 | if (isModuleEnabled('BUSINESS_DATA', enabledModules)) { 37 | modules.push(new BusinessDataApiModule(dataForSEOClient)); 38 | } 39 | if (isModuleEnabled('DOMAIN_ANALYTICS', enabledModules)) { 40 | modules.push(new DomainAnalyticsApiModule(dataForSEOClient)); 41 | } 42 | if(isModuleEnabled('CONTENT_ANALYSIS', enabledModules)) { 43 | modules.push(new ContentAnalysisApiModule(dataForSEOClient)); 44 | } 45 | 46 | return modules; 47 | } 48 | } ``` -------------------------------------------------------------------------------- /src/core/modules/dataforseo-labs/tools/google/competitor-research/google-historical-serp.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { DataForSEOClient } from '../../../../../client/dataforseo.client.js'; 3 | import { BaseTool, DataForSEOFullResponse, DataForSEOResponse } from '../../../../base.tool.js'; 4 | import { defaultGlobalToolConfig } from '../../../../../config/global.tool.js'; 5 | 6 | export class GoogleHistoricalSERP extends BaseTool { 7 | constructor(private client: DataForSEOClient) { 8 | super(client); 9 | } 10 | 11 | getName(): string { 12 | return 'dataforseo_labs_google_historical_serp'; 13 | } 14 | 15 | getDescription(): string { 16 | return `This endpoint will provide you with Google SERPs collected within the specified time frame. You will also receive a complete overview of featured snippets and other extra elements that were present within the specified dates. The data will allow you to analyze the dynamics of keyword rankings over time for the specified keyword and location.`; 17 | } 18 | 19 | getParams(): z.ZodRawShape { 20 | return { 21 | keyword: z.string().describe(`target keyword`), 22 | location_name: z.string().default("United States").describe(`full name of the location 23 | required field 24 | only in format "Country" (not "City" or "Region") 25 | example: 26 | 'United Kingdom', 'United States', 'Canada'`), 27 | language_code: z.string().default("en").describe( 28 | `language code 29 | required field 30 | example: 31 | en`) 32 | }; 33 | } 34 | 35 | async handle(params: any): Promise<any> { 36 | try { 37 | const response = await this.client.makeRequest('/v3/dataforseo_labs/google/historical_serps/live', 'POST', [{ 38 | keyword: params.keyword, 39 | location_name: params.location_name, 40 | language_code: params.language_code 41 | }]); 42 | 43 | console.error(JSON.stringify(response)); 44 | if(defaultGlobalToolConfig.fullResponse || this.supportOnlyFullResponse()){ 45 | return this.validateAndFormatResponse(response); 46 | } 47 | else { 48 | let data = response as DataForSEOResponse; 49 | this.validateResponse(data); 50 | let result = data.items; 51 | let filteredResult = result.map(item => this.filterResponseFields(item, [ 52 | "datetime", 53 | "items.type", 54 | "items.title", 55 | "items.domain", 56 | "items.rank_absolute"])); 57 | console.error(JSON.stringify(filteredResult)); 58 | return this.formatResponse(filteredResult); 59 | } 60 | } catch (error) { 61 | return this.formatErrorResponse(error); 62 | } 63 | } 64 | } ``` -------------------------------------------------------------------------------- /src/core/modules/serp/tools/serp-youtube-video-subtitles-live-advanced-tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { BaseTool } from '../../base.tool.js'; 3 | import { DataForSEOClient } from '../../../client/dataforseo.client.js'; 4 | 5 | export class SerpYoutubeVideoSubtitlesLiveAdvancedTool extends BaseTool { 6 | constructor(dataForSEOClient: DataForSEOClient) { 7 | super(dataForSEOClient); 8 | } 9 | 10 | getName(): string { 11 | return 'serp_youtube_video_subtitles_live_advanced'; 12 | } 13 | 14 | getDescription(): string { 15 | return 'provides data on the video subtitles you specify'; 16 | } 17 | 18 | getParams(): z.ZodRawShape { 19 | return { 20 | video_id: z.string().describe("ID of the video"), 21 | location_name: z.string().describe(`full name of the location 22 | required field 23 | Location format - hierarchical, comma-separated (from most specific to least) 24 | Can be one of: 25 | 1. Country only: "United States" 26 | 2. Region,Country: "California,United States" 27 | 3. City,Region,Country: "San Francisco,California,United States"`), 28 | language_code: z.string().describe("search engine language code (e.g., 'en')"), 29 | subtitles_language: z.string().optional().describe("language code of original text (e.g., 'en')"), 30 | subtitles_translate_language: z.string().optional().describe("language code of translated text (e.g., 'en')"), 31 | device: z.string().default('desktop').optional().describe(`device type 32 | optional field 33 | can take the values:desktop, mobile 34 | default value: desktop`), 35 | os: z.string().default('windows').optional().describe(`device operating system 36 | optional field 37 | if you specify desktop in the device field, choose from the following values: windows, macos 38 | default value: windows 39 | if you specify mobile in the device field, choose from the following values: android, ios 40 | default value: android`) 41 | }; 42 | } 43 | 44 | async handle(params:any): Promise<any> { 45 | try { 46 | console.error(JSON.stringify(params, null, 2)); 47 | const response = await this.dataForSEOClient.makeRequest(`/v3/serp/youtube/video_subtitles/live/advanced`, 'POST', [{ 48 | video_id: params.video_id, 49 | location_name: params.location_name, 50 | language_code: params.language_code, 51 | subtitles_language: params.subtitles_language, 52 | subtitles_translate_language: params.subtitles_translate_language, 53 | device: params.device, 54 | os: params.os, 55 | }]); 56 | return this.validateAndFormatResponse(response); 57 | } catch (error) { 58 | return this.formatErrorResponse(error); 59 | } 60 | } 61 | } 62 | 63 | 64 | ``` -------------------------------------------------------------------------------- /src/core/modules/keywords-data/tools/dataforseo-trends/dataforseo-trends-subregion-interests.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { BaseTool } from '../../../base.tool.js'; 3 | import { DataForSEOClient } from '../../../../client/dataforseo.client.js'; 4 | 5 | export class DataForSeoTrendsSubregionInterestsTool extends BaseTool { 6 | constructor(dataForSEOClient: DataForSEOClient) { 7 | super(dataForSEOClient); 8 | } 9 | 10 | getName(): string { 11 | return 'keywords_data_dataforseo_trends_subregion_interests'; 12 | } 13 | 14 | getDescription(): string { 15 | return `This endpoint will provide you with location-specific keyword popularity data from DataForSEO Trends`; 16 | } 17 | 18 | getParams(): z.ZodRawShape { 19 | return { 20 | location_name: z.string().nullable().default(null).describe(`full name of the location 21 | optional field 22 | in format "Country" 23 | example: 24 | United Kingdom`), 25 | keywords: z.array(z.string()).describe(`keywords 26 | the maximum number of keywords you can specify: 5`), 27 | type: z.enum(['web', 'news', 'ecommerce']).default('web').describe(`dataforseo trends type`), 28 | date_from: z.string().optional().describe(`starting date of the time range 29 | if you don’t specify this field, the current day and month of the preceding year will be used by default 30 | minimal value for the web type: 2004-01-01 31 | minimal value for other types: 2008-01-01 32 | date format: "yyyy-mm-dd" 33 | example: 34 | "2019-01-15"`), 35 | date_to: z.string().optional() 36 | .describe( 37 | `ending date of the time range 38 | if you don’t specify this field, the today’s date will be used by default 39 | date format: "yyyy-mm-dd" 40 | example: 41 | "2019-01-15"`), 42 | time_range: z.enum(['past_4_hours', 'past_day', 'past_7_days', 'past_30_days', 'past_90_days', 'past_12_months', 'past_5_years']) 43 | .default('past_7_days') 44 | .describe( 45 | `preset time ranges 46 | if you specify date_from or date_to parameters, this field will be ignored when setting a task`), 47 | }; 48 | } 49 | 50 | async handle(params: any): Promise<any> { 51 | try { 52 | const response = await this.dataForSEOClient.makeRequest('/v3/keywords_data/dataforseo_trends/subregion_interests/live', 'POST', [{ 53 | location_name: params.location_name, 54 | keywords: params.keywords, 55 | type: params.type, 56 | date_from: params.date_from, 57 | date_to: params.date_to, 58 | time_range: params.time_range, 59 | }]); 60 | return this.validateAndFormatResponse(response); 61 | } catch (error) { 62 | return this.formatErrorResponse(error); 63 | } 64 | } 65 | } ``` -------------------------------------------------------------------------------- /src/core/modules/backlinks/tools/backlinks-bulk-new-lost-backlinks.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { DataForSEOClient } from '../../../client/dataforseo.client.js'; 3 | import { BaseTool } from '../../base.tool.js'; 4 | 5 | export class BacklinksBulkNewLostBacklinksTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'backlinks_bulk_new_lost_backlinks'; 12 | } 13 | 14 | getDescription(): string { 15 | return `This endpoint will provide you with the number of referring domains pointing to domains, subdomains, and pages specified in the targets array. The returned numbers are based on all live referring domains, that is, total number of domains pointing to the target with any type of backlinks (e.g., nofollow, noreferrer, ugc, sponsored etc) that were found during the latest check. 16 | Note that if you indicate a domain as a target, you will get result for the root domain (domain with all of its subdomains), e.g. dataforseo.com and app.dataforseo.com`; 17 | } 18 | 19 | getParams(): z.ZodRawShape { 20 | return { 21 | targets: z.array(z.string()).describe(`domains, subdomains or webpages to get rank for 22 | required field 23 | you can set up to 1000 domains, subdomains or webpages 24 | the domain or subdomain should be specified without https:// and www. 25 | the page should be specified with absolute URL (including http:// or https://) 26 | example: 27 | "targets": [ 28 | "forbes.com", 29 | "cnn.com", 30 | "bbc.com", 31 | "yelp.com", 32 | "https://www.apple.com/iphone/", 33 | "https://ahrefs.com/blog/", 34 | "ibm.com", 35 | "https://variety.com/", 36 | "https://stackoverflow.com/", 37 | "www.trustpilot.com" 38 | ]`), 39 | date_from: z.string().optional().describe(`starting date of the time range 40 | optional field 41 | this field indicates the date which will be used as a threshold for new and lost backlinks; 42 | the backlinks that appeared in our index after the specified date will be considered as new; 43 | the backlinks that weren’t found after the specified date, but were present before, will be considered as lost; 44 | default value: today’s date -(minus) one month; 45 | e.g. if today is 2021-10-13, default date_from will be 2021-09-13. 46 | minimum value equals today’s date -(minus) one year; 47 | e.g. if today is 2021-10-13, minimum date_from will be 2020-10-13. 48 | 49 | date format: "yyyy-mm-dd" 50 | example: 51 | "2021-01-01"`) 52 | }; 53 | } 54 | 55 | async handle(params: any): Promise<any> { 56 | try { 57 | const response = await this.client.makeRequest('/v3/backlinks/bulk_new_lost_backlinks/live', 'POST', [{ 58 | targets: params.targets 59 | }]); 60 | return this.validateAndFormatResponse(response); 61 | } catch (error) { 62 | return this.formatErrorResponse(error); 63 | } 64 | } 65 | } ``` -------------------------------------------------------------------------------- /src/core/modules/keywords-data/tools/dataforseo-trends/dataforseo-trends-demography.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { BaseTool } from '../../../base.tool.js'; 3 | import { DataForSEOClient } from '../../../../client/dataforseo.client.js'; 4 | 5 | export class DataForSeoTrendsDemographyTool extends BaseTool { 6 | constructor(dataForSEOClient: DataForSEOClient) { 7 | super(dataForSEOClient); 8 | } 9 | 10 | getName(): string { 11 | return 'keywords_data_dataforseo_trends_demography'; 12 | } 13 | 14 | getDescription(): string { 15 | return `This endpoint will provide you with the demographic breakdown (by age and gender) of keyword popularity per each specified term based on DataForSEO Trends data`; 16 | } 17 | 18 | getParams(): z.ZodRawShape { 19 | return { 20 | location_name: z.string().nullable().default(null).describe(`full name of the location 21 | optional field 22 | in format "Country" 23 | example: 24 | United Kingdom`), 25 | keywords: z.array(z.string()).describe(`keywords 26 | the maximum number of keywords you can specify: 5`), 27 | type: z.enum(['web', 'news', 'ecommerce']).default('web').describe(`dataforseo trends type`), 28 | date_from: z.string().optional().describe(`starting date of the time range 29 | if you don’t specify this field, the current day and month of the preceding year will be used by default 30 | minimal value for the web type: 2004-01-01 31 | minimal value for other types: 2008-01-01 32 | date format: "yyyy-mm-dd" 33 | example: 34 | "2019-01-15"`), 35 | date_to: z.string().optional() 36 | .describe( 37 | `ending date of the time range 38 | if you don’t specify this field, the today’s date will be used by default 39 | date format: "yyyy-mm-dd" 40 | example: 41 | "2019-01-15"`), 42 | time_range: z.enum(['past_4_hours', 'past_day', 'past_7_days', 'past_30_days', 'past_90_days', 'past_12_months', 'past_5_years']) 43 | .default('past_7_days') 44 | .describe( 45 | `preset time ranges 46 | if you specify date_from or date_to parameters, this field will be ignored when setting a task`), 47 | }; 48 | } 49 | 50 | async handle(params: any): Promise<any> { 51 | try { 52 | const response = await this.dataForSEOClient.makeRequest('/v3/keywords_data/dataforseo_trends/demography/live', 'POST', [{ 53 | location_name: params.location_name, 54 | keywords: params.keywords, 55 | type: params.type, 56 | date_from: params.date_from, 57 | date_to: params.date_to, 58 | time_range: params.time_range, 59 | }]); 60 | return this.validateAndFormatResponse(response); 61 | } catch (error) { 62 | return this.formatErrorResponse(error); 63 | } 64 | } 65 | } ``` -------------------------------------------------------------------------------- /src/core/modules/keywords-data/tools/dataforseo-trends/dataforseo-trends-explore.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { BaseTool } from '../../../base.tool.js'; 3 | import { DataForSEOClient } from '../../../../client/dataforseo.client.js'; 4 | 5 | export class DataForSeoTrendsExploreTool extends BaseTool { 6 | constructor(dataForSEOClient: DataForSEOClient) { 7 | super(dataForSEOClient); 8 | } 9 | 10 | getName(): string { 11 | return 'keywords_data_dataforseo_trends_explore'; 12 | } 13 | 14 | getDescription(): string { 15 | return `This endpoint will provide you with the keyword popularity data from DataForSEO Trends. You can check keyword trends for Google Search, Google News, and Google Shopping`; 16 | } 17 | 18 | getParams(): z.ZodRawShape { 19 | return { 20 | location_name: z.string().nullable().default(null).describe(`full name of the location 21 | optional field 22 | in format "Country" 23 | example: 24 | United Kingdom`), 25 | keywords: z.array(z.string()).describe(`keywords 26 | the maximum number of keywords you can specify: 5`), 27 | type: z.enum(['web', 'news', 'ecommerce']).default('web').describe(`dataforseo trends type`), 28 | date_from: z.string().optional().describe(`starting date of the time range 29 | if you don’t specify this field, the current day and month of the preceding year will be used by default 30 | minimal value for the web type: 2004-01-01 31 | minimal value for other types: 2008-01-01 32 | date format: "yyyy-mm-dd" 33 | example: 34 | "2019-01-15"`), 35 | date_to: z.string().optional() 36 | .describe( 37 | `ending date of the time range 38 | if you don’t specify this field, the today’s date will be used by default 39 | date format: "yyyy-mm-dd" 40 | example: 41 | "2019-01-15"`), 42 | time_range: z.enum(['past_4_hours', 'past_day', 'past_7_days', 'past_30_days', 'past_90_days', 'past_12_months', 'past_5_years']) 43 | .default('past_7_days') 44 | .describe( 45 | `preset time ranges 46 | if you specify date_from or date_to parameters, this field will be ignored when setting a task`), 47 | }; 48 | } 49 | 50 | async handle(params: any): Promise<any> { 51 | try { 52 | const response = await this.dataForSEOClient.makeRequest('/v3/keywords_data/dataforseo_trends/explore/live', 'POST', [{ 53 | location_name: params.location_name, 54 | keywords: params.keywords, 55 | type: params.type, 56 | date_from: params.date_from, 57 | date_to: params.date_to, 58 | time_range: params.time_range, 59 | }]); 60 | return this.validateAndFormatResponse(response); 61 | } catch (error) { 62 | return this.formatErrorResponse(error); 63 | } 64 | } 65 | } ``` -------------------------------------------------------------------------------- /src/core/modules/serp/tools/serp-organic-live-advanced.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { BaseTool } from '../../base.tool.js'; 3 | import { DataForSEOClient } from '../../../client/dataforseo.client.js'; 4 | 5 | export class SerpOrganicLiveAdvancedTool extends BaseTool { 6 | constructor(dataForSEOClient: DataForSEOClient) { 7 | super(dataForSEOClient); 8 | } 9 | 10 | getName(): string { 11 | return 'serp_organic_live_advanced'; 12 | } 13 | 14 | getDescription(): string { 15 | return 'Get organic search results for a keyword in specified search engine'; 16 | } 17 | 18 | getParams(): z.ZodRawShape { 19 | return { 20 | search_engine: z.string().default('google').describe("search engine name, one of: google, yahoo, bing."), 21 | location_name: z.string().default('United States').describe(`full name of the location 22 | required field 23 | Location format - hierarchical, comma-separated (from most specific to least) 24 | Can be one of: 25 | 1. Country only: "United States" 26 | 2. Region,Country: "California,United States" 27 | 3. City,Region,Country: "San Francisco,California,United States"`), 28 | depth: z.number().min(10).max(700).default(10).describe(`parsing depth 29 | optional field 30 | number of results in SERP`), 31 | language_code: z.string().describe("search engine language code (e.g., 'en')"), 32 | keyword: z.string().describe("Search keyword"), 33 | max_crawl_pages: z.number().min(1).max(7).optional().default(1).describe(`page crawl limit 34 | optional field 35 | number of search results pages to crawl 36 | max value: 100 37 | Note: the max_crawl_pages and depth parameters complement each other`), 38 | device: z.string().default('desktop').optional().describe(`device type 39 | optional field 40 | can take the values:desktop, mobile 41 | default value: desktop`), 42 | people_also_ask_click_depth: z.number().min(1).max(4).optional() 43 | .describe(`clicks on the corresponding element 44 | specify the click depth on the people_also_ask element to get additional people_also_ask_element items;`) 45 | }; 46 | } 47 | 48 | async handle(params:any): Promise<any> { 49 | try { 50 | console.error(JSON.stringify(params, null, 2)); 51 | const response = await this.dataForSEOClient.makeRequest(`/v3/serp/${params.search_engine}/organic/live/advanced`, 'POST', [{ 52 | location_name: params.location_name, 53 | language_code: params.language_code, 54 | keyword: params.keyword, 55 | depth: params.depth, 56 | max_crawl_pages: params.max_crawl_pages, 57 | device: params.device, 58 | people_also_ask_click_depth: params.people_also_ask_click_depth && params.people_also_ask_click_depth > 0 ? params.people_also_ask_click_depth : undefined, 59 | }]); 60 | return this.validateAndFormatResponse(response); 61 | } catch (error) { 62 | return this.formatErrorResponse(error); 63 | } 64 | } 65 | } ``` -------------------------------------------------------------------------------- /src/core/modules/domain-analytics/tools/whois/whois-overview.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { DataForSEOClient } from '../../../../client/dataforseo.client.js'; 3 | import { BaseTool } from '../../../base.tool.js'; 4 | 5 | export class WhoisOverviewTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'domain_analytics_whois_overview'; 12 | } 13 | 14 | getDescription(): string { 15 | return `This endpoint will provide you with Whois data enriched with backlink stats, and ranking and traffic info from organic and paid search results. Using this endpoint you will be able to get all these data for the domains matching the parameters you specify in the request`; 16 | } 17 | 18 | getParams(): z.ZodRawShape { 19 | return { 20 | limit: z.number().min(1).max(1000).default(10).optional().describe("the maximum number of returned domains"), 21 | offset: z.number().min(0).optional().describe( 22 | `offset in the results array of returned businesses 23 | optional field 24 | default value: 0 25 | if you specify the 10 value, the first ten entities in the results array will be omitted and the data will be provided for the successive entities` 26 | ), 27 | filters: this.getFilterExpression().optional().describe( 28 | `array of results filtering parameters 29 | optional field 30 | you can add several filters at once (8 filters maximum) 31 | you should set a logical operator and, or between the conditions 32 | the following operators are supported: 33 | regex, not_regex, <, <=, >, >=, =, <>, in, not_in, like, not_like, match, not_match 34 | you can use the % operator with like and not_like to match any string of zero or more characters 35 | example: 36 | ["rating.value",">",3]` 37 | ), 38 | order_by: z.array(z.string()).optional().describe( 39 | `results sorting rules 40 | optional field 41 | you can use the same values as in the filters array to sort the results 42 | possible sorting types: 43 | asc – results will be sorted in the ascending order 44 | desc – results will be sorted in the descending order 45 | you should use a comma to set up a sorting parameter 46 | example: 47 | ["rating.value,desc"]note that you can set no more than three sorting rules in a single request 48 | you should use a comma to separate several sorting rules 49 | example: 50 | ["rating.value,desc","rating.votes_count,desc"]` 51 | ), 52 | is_claimed: z.boolean().optional().describe(`indicates whether the business is verified by its owner on Google Maps`).default(true) 53 | }; 54 | } 55 | 56 | async handle(params: any): Promise<any> { 57 | try { 58 | const response = await this.client.makeRequest('/v3/domain_analytics/whois/overview/live', 'POST', [{ 59 | limit: params.limit, 60 | offset: params.offset, 61 | filters: this.formatFilters(params.filters), 62 | order_by: this.formatOrderBy(params.order_by), 63 | }]); 64 | return this.validateAndFormatResponse(response); 65 | } catch (error) { 66 | return this.formatErrorResponse(error); 67 | } 68 | } 69 | } ``` -------------------------------------------------------------------------------- /src/core/modules/backlinks/tools/backlinks-timeseries-summary.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { DataForSEOClient } from '../../../client/dataforseo.client.js'; 3 | import { BaseTool } from '../../base.tool.js'; 4 | 5 | export class BacklinksTimeseriesSummaryTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'backlinks_timeseries_summary'; 12 | } 13 | 14 | getDescription(): string { 15 | return `This endpoint will provide you with an overview of backlink data for the target domain available during a period between the two indicated dates. Backlink metrics will be grouped by the time range that you define: day, week, month, or year. 16 | Data from this endpoint will be especially helpful for building time-series graphs of daily, weekly, monthly, and yearly link-building progress`; 17 | } 18 | 19 | getParams(): z.ZodRawShape { 20 | return { 21 | target: z.string().describe(`domain to get data for 22 | required field 23 | a domain should be specified without https:// and www. 24 | example: 25 | "forbes.com"`), 26 | date_from: z.string().describe(`starting date of the time range 27 | optional field 28 | this field indicates the date which will be used as a threshold for summary data; 29 | minimum value: 2019-01-30 30 | maximum value shouldn’t exceed the date specified in the date_to 31 | date format: "yyyy-mm-dd" 32 | example: 33 | "2021-01-01"`).optional(), 34 | date_to: z.string().describe(`ending date of the time range 35 | optional field 36 | if you don’t specify this field, the today’s date will be used by default 37 | minimum value shouldn’t preceed the date specified in the date_from 38 | maximum value: today’s date 39 | date format: "yyyy-mm-dd" 40 | example: 41 | "2021-01-15"`).optional(), 42 | group_range: z.string().optional().describe(`time range which will be used to group the results 43 | optional field 44 | default value: month 45 | possible values: day, week, month, year 46 | note: for day, we will return items corresponding to all dates between and including date_from and date_to; 47 | for week/month/year, we will return items corresponding to full weeks/months/years, where each item will indicate the last day of the week/month/year 48 | 49 | for example, if you specify: 50 | "group_range": "month", 51 | "date_from": "2022-03-23", 52 | "date_to": "2022-05-13" 53 | we will return items falling between 2022-03-01 and 2022-05-31, namely, three items corresponding to the following dates: 2022-03-31, 2022-04-30, 2022-05-31 54 | 55 | if there is no data for a certain day/week/month/year, we will return 0`).default('month') 56 | }; 57 | } 58 | 59 | async handle(params: any): Promise<any> { 60 | try { 61 | const response = await this.client.makeRequest('/v3/backlinks/timeseries_summary/live', 'POST', [{ 62 | target: params.target, 63 | date_from: params.date_from, 64 | date_to: params.date_to, 65 | group_range: params.group_range 66 | }]); 67 | return this.validateAndFormatResponse(response); 68 | } catch (error) { 69 | return this.formatErrorResponse(error); 70 | } 71 | } 72 | } ``` -------------------------------------------------------------------------------- /src/core/config/field-configuration.ts: -------------------------------------------------------------------------------- ```typescript 1 | import * as fs from 'fs'; 2 | 3 | export interface FieldConfiguration { 4 | supported_fields: Record<string, string[]>; 5 | } 6 | 7 | export class FieldConfigurationManager { 8 | private static instance: FieldConfigurationManager; 9 | private config: FieldConfiguration | null = null; 10 | 11 | private constructor() {} 12 | 13 | public static getInstance(): FieldConfigurationManager { 14 | if (!FieldConfigurationManager.instance) { 15 | FieldConfigurationManager.instance = new FieldConfigurationManager(); 16 | } 17 | return FieldConfigurationManager.instance; 18 | } 19 | 20 | public loadFromFile(configPath: string): void { 21 | try { 22 | if (!fs.existsSync(configPath)) { 23 | console.warn(`Configuration file not found: ${configPath}`); 24 | return; 25 | } 26 | console.error(`Loading field configuration from: ${configPath}`); 27 | const configContent = fs.readFileSync(configPath, 'utf8'); 28 | const parsedConfig = JSON.parse(configContent); 29 | 30 | // Validate the configuration structure 31 | if (!parsedConfig.supported_fields || typeof parsedConfig.supported_fields !== 'object') { 32 | throw new Error('Invalid configuration format. Expected { "supported_fields": { "tool_name": ["field1", "field2"] } }'); 33 | } 34 | 35 | this.config = parsedConfig; 36 | console.log(`Field configuration loaded from: ${configPath}`); 37 | } catch (error) { 38 | console.error('Error loading field configuration:', error); 39 | throw error; 40 | } 41 | } 42 | 43 | public getFieldsForTool(toolName: string): string[] | null { 44 | if (!this.config) { 45 | return null; // No configuration loaded, return all fields 46 | } 47 | 48 | return this.config.supported_fields[toolName] || null; 49 | } 50 | 51 | public hasConfiguration(): boolean { 52 | return this.config !== null; 53 | } 54 | 55 | public isToolConfigured(toolName: string): boolean { 56 | return this.config !== null && toolName in this.config.supported_fields; 57 | } 58 | 59 | public getConfiguration(): FieldConfiguration | null { 60 | return this.config; 61 | } 62 | 63 | public clearConfiguration(): void { 64 | this.config = null; 65 | } 66 | } 67 | 68 | // Helper functions for easy access 69 | export function getFieldsForTool(toolName: string): string[] | null { 70 | return FieldConfigurationManager.getInstance().getFieldsForTool(toolName); 71 | } 72 | 73 | export function isToolConfigured(toolName: string): boolean { 74 | return FieldConfigurationManager.getInstance().isToolConfigured(toolName); 75 | } 76 | 77 | export function hasFieldConfiguration(): boolean { 78 | return FieldConfigurationManager.getInstance().hasConfiguration(); 79 | } 80 | 81 | export function loadFieldConfiguration(configPath: string): void { 82 | FieldConfigurationManager.getInstance().loadFromFile(configPath); 83 | } 84 | 85 | export function initializeFieldConfiguration(): void { 86 | const configPath = process.env.FIELD_CONFIG_PATH; 87 | 88 | if (configPath) { 89 | try { 90 | loadFieldConfiguration(configPath); 91 | } catch (error) { 92 | console.error('Failed to load field configuration:', error); 93 | } 94 | } 95 | } ``` -------------------------------------------------------------------------------- /src/core/modules/backlinks/tools/backlinks-timeseries-new-lost-summary.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { DataForSEOClient } from '../../../client/dataforseo.client.js'; 3 | import { BaseTool } from '../../base.tool.js'; 4 | 5 | export class BacklinksTimeseriesNewLostSummaryTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'backlinks_timeseries_new_lost_summary'; 12 | } 13 | 14 | getDescription(): string { 15 | return `This endpoint will provide you with the number of new and lost backlinks and referring domains for the domain specified in the target field. 16 | The results will be provided for a period between the two indicated dates, and metrics will be grouped by the time range that you define: day, week, month, or year. 17 | Data from this endpoint will be especially helpful for building time-series graphs of new and lost backlinks and referring domains. 18 | 19 | `; 20 | } 21 | 22 | getParams(): z.ZodRawShape { 23 | return { 24 | target: z.string().describe(`domain to get data for 25 | required field 26 | a domain should be specified without https:// and www. 27 | example: 28 | "forbes.com"`), 29 | date_from: z.string().describe(`starting date of the time range 30 | optional field 31 | this field indicates the date which will be used as a threshold for summary data; 32 | minimum value: 2019-01-30 33 | maximum value shouldn’t exceed the date specified in the date_to 34 | date format: "yyyy-mm-dd" 35 | example: 36 | "2021-01-01"`).optional(), 37 | date_to: z.string().describe(`ending date of the time range 38 | optional field 39 | if you don’t specify this field, the today’s date will be used by default 40 | minimum value shouldn’t preceed the date specified in the date_from 41 | maximum value: today’s date 42 | date format: "yyyy-mm-dd" 43 | example: 44 | "2021-01-15"`).optional(), 45 | group_range: z.string().optional().describe(`time range which will be used to group the results 46 | optional field 47 | default value: month 48 | possible values: day, week, month, year 49 | note: for day, we will return items corresponding to all dates between and including date_from and date_to; 50 | for week/month/year, we will return items corresponding to full weeks/months/years, where each item will indicate the last day of the week/month/year 51 | 52 | for example, if you specify: 53 | "group_range": "month", 54 | "date_from": "2022-03-23", 55 | "date_to": "2022-05-13" 56 | we will return items falling between 2022-03-01 and 2022-05-31, namely, three items corresponding to the following dates: 2022-03-31, 2022-04-30, 2022-05-31 57 | 58 | if there is no data for a certain day/week/month/year, we will return 0`).default('month') 59 | }; 60 | } 61 | 62 | async handle(params: any): Promise<any> { 63 | try { 64 | const response = await this.client.makeRequest('/v3/backlinks/timeseries_new_lost_summary/live', 'POST', [{ 65 | target: params.target, 66 | date_from: params.date_from, 67 | date_to: params.date_to, 68 | group_range: params.group_range 69 | }]); 70 | return this.validateAndFormatResponse(response); 71 | } catch (error) { 72 | return this.formatErrorResponse(error); 73 | } 74 | } 75 | } ``` -------------------------------------------------------------------------------- /src/core/utils/field-filter.ts: -------------------------------------------------------------------------------- ```typescript 1 | type FieldPath = string | string[]; 2 | 3 | export function filterFields(data: any, fields: FieldPath[]): any { 4 | if (!data || !fields || fields.length === 0) { 5 | return data; 6 | } 7 | 8 | const result: any = {}; 9 | 10 | fields.forEach(field => { 11 | const path = Array.isArray(field) ? field : field.split('.'); 12 | extractAndSetValue(data, result, path); 13 | }); 14 | 15 | return result; 16 | } 17 | 18 | function extractAndSetValue(source: any, target: any, path: string[]): void { 19 | if (path.length === 0) return; 20 | 21 | const [currentKey, ...remainingPath] = path; 22 | 23 | if (remainingPath.length === 0) { 24 | // This is the final key, extract the value 25 | if (currentKey === '*') { 26 | // Wildcard at the end - copy all properties/items 27 | if (Array.isArray(source)) { 28 | Object.assign(target, source); 29 | } else if (source && typeof source === 'object') { 30 | Object.assign(target, source); 31 | } 32 | } else if (source && typeof source === 'object' && currentKey in source) { 33 | target[currentKey] = source[currentKey]; 34 | } 35 | return; 36 | } 37 | 38 | // Not the final key, need to go deeper 39 | if (currentKey === '*') { 40 | // Wildcard in the middle of the path 41 | if (Array.isArray(source)) { 42 | // Handle array with wildcard 43 | if (!Array.isArray(target)) { 44 | // Convert target to array to match source structure 45 | Object.keys(target).forEach(key => delete target[key]); 46 | Object.setPrototypeOf(target, Array.prototype); 47 | target.length = 0; 48 | } 49 | 50 | source.forEach((item, index) => { 51 | if (!target[index]) { 52 | target[index] = {}; 53 | } 54 | extractAndSetValue(item, target[index], remainingPath); 55 | }); 56 | } else if (source && typeof source === 'object') { 57 | // Handle object with wildcard 58 | Object.keys(source).forEach(key => { 59 | if (!target[key]) { 60 | target[key] = {}; 61 | } 62 | extractAndSetValue(source[key], target[key], remainingPath); 63 | }); 64 | } 65 | } else if (source && typeof source === 'object' && currentKey in source) { 66 | // Regular key handling 67 | const sourceValue = source[currentKey]; 68 | 69 | if (Array.isArray(sourceValue)) { 70 | // Handle array - preserve array structure 71 | if (!target[currentKey]) { 72 | target[currentKey] = []; 73 | } 74 | 75 | sourceValue.forEach((item, index) => { 76 | if (!target[currentKey][index]) { 77 | target[currentKey][index] = {}; 78 | } 79 | extractAndSetValue(item, target[currentKey][index], remainingPath); 80 | }); 81 | } else if (sourceValue && typeof sourceValue === 'object') { 82 | // Handle object 83 | if (!target[currentKey]) { 84 | target[currentKey] = {}; 85 | } 86 | extractAndSetValue(sourceValue, target[currentKey], remainingPath); 87 | } 88 | } 89 | } 90 | 91 | export function parseFieldPaths(fields: string[]): FieldPath[] { 92 | return fields.map(field => { 93 | // Handle array notation 94 | if (field.includes('[')) { 95 | const [base, index] = field.split('['); 96 | return [base, index.replace(']', '')]; 97 | } 98 | return field; 99 | }); 100 | } 101 | ``` -------------------------------------------------------------------------------- /src/core/modules/backlinks/tools/backlinks-anchor.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { DataForSEOClient } from '../../../client/dataforseo.client.js'; 3 | import { BaseTool } from '../../base.tool.js'; 4 | 5 | export class BacklinksAnchorTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'backlinks_anchors'; 12 | } 13 | 14 | getDescription(): string { 15 | return "This endpoint will provide you with a detailed overview of anchors used when linking to the specified website with relevant backlink data for each of them"; 16 | } 17 | 18 | getParams(): z.ZodRawShape { 19 | return { 20 | target: z.string().describe(`domain, subdomain or webpage to get backlinks for 21 | required field 22 | a domain or a subdomain should be specified without https:// and www. 23 | a page should be specified with absolute URL (including http:// or https://)`), 24 | limit: z.number().min(1).max(1000).default(10).optional().describe("the maximum number of returned anchors"), 25 | offset: z.number().min(0).optional().describe( 26 | `offset in the results array of returned anchors 27 | optional field 28 | default value: 0 29 | if you specify the 10 value, the first ten anchors in the results array will be omitted and the data will be provided for the successive anchors` 30 | ), 31 | filters: this.getFilterExpression().optional().describe( 32 | `array of results filtering parameters 33 | optional field 34 | you can add several filters at once (8 filters maximum) 35 | you should set a logical operator and, or between the conditions 36 | the following operators are supported: 37 | =, <>, in, not_in, like, not_like, ilike, not_ilike, regex, not_regex, match, not_match 38 | you can use the % operator with like and not_like to match any string of zero or more characters 39 | example: 40 | ["rank",">","80"] 41 | [["page_from_rank",">","55"], 42 | "and", 43 | ["dofollow","=",true]] 44 | 45 | [["first_seen",">","2017-10-23 11:31:45 +00:00"], 46 | "and", 47 | [["anchor","like","%seo%"],"or",["text_pre","like","%seo%"]]]` 48 | ), 49 | order_by: z.array(z.string()).optional().describe( 50 | `results sorting rules 51 | optional field 52 | you can use the same values as in the filters array to sort the results 53 | possible sorting types: 54 | asc – results will be sorted in the ascending order 55 | desc – results will be sorted in the descending order 56 | you should use a comma to set up a sorting type 57 | example: 58 | ["rank,desc"] 59 | note that you can set no more than three sorting rules in a single request 60 | you should use a comma to separate several sorting rules 61 | example: 62 | ["domain_from_rank,desc","page_from_rank,asc"]` 63 | ), 64 | }; 65 | } 66 | 67 | async handle(params: any): Promise<any> { 68 | try { 69 | const response = await this.client.makeRequest('/v3/backlinks/anchors/live', 'POST', [{ 70 | target: params.target, 71 | limit: params.limit, 72 | offset: params.offset, 73 | filters: this.formatFilters(params.filters), 74 | order_by: this.formatOrderBy(params.order_by) 75 | }]); 76 | return this.validateAndFormatResponse(response); 77 | } catch (error) { 78 | return this.formatErrorResponse(error); 79 | } 80 | } 81 | } ``` -------------------------------------------------------------------------------- /src/core/modules/backlinks/tools/backlinks-domain-pages.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { DataForSEOClient } from '../../../client/dataforseo.client.js'; 3 | import { BaseTool } from '../../base.tool.js'; 4 | 5 | export class BacklinksDomainPagesTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'backlinks_domain_pages'; 12 | } 13 | 14 | getDescription(): string { 15 | return "This endpoint will provide you with a detailed overview of domain pages with backlink data for each page"; 16 | } 17 | 18 | getParams(): z.ZodRawShape { 19 | return { 20 | target: z.string().describe(`domain, subdomain or webpage to get backlinks for 21 | required field 22 | a domain or a subdomain should be specified without https:// and www. 23 | a page should be specified with absolute URL (including http:// or https://)`), 24 | limit: z.number().min(1).max(1000).default(10).optional().describe("the maximum number of returned pages"), 25 | offset: z.number().min(0).optional().describe( 26 | `offset in the results array of returned pages 27 | optional field 28 | default value: 0 29 | if you specify the 10 value, the first ten pages in the results array will be omitted and the data will be provided for the successive pages` 30 | ), 31 | filters: this.getFilterExpression().optional().describe( 32 | `array of results filtering parameters 33 | optional field 34 | you can add several filters at once (8 filters maximum) 35 | you should set a logical operator and, or between the conditions 36 | the following operators are supported: 37 | regex, not_regex, =, <>, in, not_in, like, not_like, ilike, not_ilike, match, not_match 38 | you can use the % operator with like and not_like to match any string of zero or more characters 39 | example: 40 | ["meta.internal_links_count",">","1"] 41 | [["meta.external_links_count",">","2"], 42 | "and", 43 | ["backlinks",">","10"]] 44 | 45 | [["first_visited",">","2017-10-23 11:31:45 +00:00"], 46 | "and", 47 | [["title","like","%seo%"],"or",["referring_domains",">","10"]]]` 48 | ), 49 | order_by: z.array(z.string()).optional().describe( 50 | `results sorting rules 51 | optional field 52 | you can use the same values as in the filters array to sort the results 53 | possible sorting types: 54 | asc – results will be sorted in the ascending order 55 | desc – results will be sorted in the descending order 56 | you should use a comma to set up a sorting type 57 | example: 58 | ["page_summary.backlinks,desc"] 59 | note that you can set no more than three sorting rules in a single request 60 | you should use a comma to separate several sorting rules 61 | example: 62 | ["page_summary.backlinks,desc","page_summary.rank,asc"]` 63 | ), 64 | }; 65 | } 66 | 67 | async handle(params: any): Promise<any> { 68 | try { 69 | const response = await this.client.makeRequest('/v3/backlinks/domain_pages/live', 'POST', [{ 70 | target: params.target, 71 | limit: params.limit, 72 | offset: params.offset, 73 | filters: this.formatFilters(params.filters), 74 | order_by: this.formatOrderBy(params.order_by), 75 | }]); 76 | return this.validateAndFormatResponse(response); 77 | } catch (error) { 78 | return this.formatErrorResponse(error); 79 | } 80 | } 81 | } ``` -------------------------------------------------------------------------------- /src/core/modules/backlinks/tools/backlinks-referring-domains.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { DataForSEOClient } from '../../../client/dataforseo.client.js'; 3 | import { BaseTool } from '../../base.tool.js'; 4 | 5 | export class BacklinksReferringDomainsTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'backlinks_referring_domains'; 12 | } 13 | 14 | getDescription(): string { 15 | return "This endpoint will provide you with a detailed overview of referring domains pointing to the target you specify"; 16 | } 17 | 18 | getParams(): z.ZodRawShape { 19 | return { 20 | target: z.string().describe(`domain, subdomain or webpage to get backlinks for 21 | required field 22 | a domain or a subdomain should be specified without https:// and www. 23 | a page should be specified with absolute URL (including http:// or https://)`), 24 | limit: z.number().min(1).max(1000).default(10).optional().describe("the maximum number of returned pages"), 25 | offset: z.number().min(0).optional().describe( 26 | `offset in the results array of returned pages 27 | optional field 28 | default value: 0 29 | if you specify the 10 value, the first ten pages in the results array will be omitted and the data will be provided for the successive pages` 30 | ), 31 | filters: this.getFilterExpression().optional().describe( 32 | `array of results filtering parameters 33 | optional field 34 | you can add several filters at once (8 filters maximum) 35 | you should set a logical operator and, or between the conditions 36 | the following operators are supported: 37 | regex, not_regex, =, <>, in, not_in, like, not_like, ilike, not_ilike, match, not_match 38 | you can use the % operator with like and not_like to match any string of zero or more characters 39 | example: 40 | ["meta.internal_links_count",">","1"] 41 | [["meta.external_links_count",">","2"], 42 | "and", 43 | ["backlinks",">","10"]] 44 | 45 | [["first_visited",">","2017-10-23 11:31:45 +00:00"], 46 | "and", 47 | [["title","like","%seo%"],"or",["referring_domains",">","10"]]]` 48 | ), 49 | order_by: z.array(z.string()).optional().describe( 50 | `results sorting rules 51 | optional field 52 | you can use the same values as in the filters array to sort the results 53 | possible sorting types: 54 | asc – results will be sorted in the ascending order 55 | desc – results will be sorted in the descending order 56 | you should use a comma to set up a sorting type 57 | example: 58 | ["page_summary.backlinks,desc"] 59 | note that you can set no more than three sorting rules in a single request 60 | you should use a comma to separate several sorting rules 61 | example: 62 | ["page_summary.backlinks,desc","page_summary.rank,asc"]` 63 | ), 64 | }; 65 | } 66 | 67 | async handle(params: any): Promise<any> { 68 | try { 69 | const response = await this.client.makeRequest('/v3/backlinks/referring_domains/live', 'POST', [{ 70 | target: params.target, 71 | limit: params.limit, 72 | offset: params.offset, 73 | filters: this.formatFilters(params.filters), 74 | order_by: this.formatOrderBy(params.order_by), 75 | }]); 76 | return this.validateAndFormatResponse(response); 77 | } catch (error) { 78 | return this.formatErrorResponse(error); 79 | } 80 | } 81 | } ``` -------------------------------------------------------------------------------- /src/core/modules/business-data-api/tools/listings/business-listings-filters.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { DataForSEOClient } from '../../../../client/dataforseo.client.js'; 3 | import { BaseTool, DataForSEOFullResponse } from '../../../base.tool.js'; 4 | 5 | interface FilterField { 6 | type: string; 7 | path: string; 8 | } 9 | 10 | interface ToolFilters { 11 | [key: string]: { 12 | [field: string]: string; 13 | }; 14 | } 15 | 16 | export class BusinessListingsFiltersTool extends BaseTool { 17 | private static cache: ToolFilters | null = null; 18 | private static lastFetchTime: number = 0; 19 | private static readonly CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours in milliseconds 20 | 21 | // Map of tool names to their corresponding filter paths in the API response 22 | private static readonly TOOL_TO_FILTER_MAP: { [key: string]: string } = { 23 | 'business_data_business_listings_search': 'search', 24 | 'business_data_business_listings_categories_aggregation': 'categories_aggregation' 25 | }; 26 | 27 | constructor(private client: DataForSEOClient) { 28 | super(client); 29 | } 30 | 31 | getName(): string { 32 | return 'business_data_business_listings_filters'; 33 | } 34 | 35 | getDescription(): string { 36 | return `Here you will find all the necessary information about filters that can be used with Business Data API business listings endpoints. 37 | 38 | Please, keep in mind that filters are associated with a certain object in the result array, and should be specified accordingly.`; 39 | } 40 | 41 | protected supportOnlyFullResponse(): boolean { 42 | return true; 43 | } 44 | 45 | getParams(): z.ZodRawShape { 46 | return { 47 | tool: z.string().optional().describe('The name of the tool to get filters for') 48 | }; 49 | } 50 | 51 | private async fetchAndCacheFilters(): Promise<ToolFilters> { 52 | const now = Date.now(); 53 | 54 | // Return cached data if it's still valid 55 | if (BusinessListingsFiltersTool.cache && 56 | (now - BusinessListingsFiltersTool.lastFetchTime) < BusinessListingsFiltersTool.CACHE_TTL) { 57 | return BusinessListingsFiltersTool.cache; 58 | } 59 | 60 | // Fetch fresh data 61 | const response = await this.client.makeRequest('/v3/business_data/business_listings/available_filters', 'GET', null, true) as DataForSEOFullResponse; 62 | this.validateResponseFull(response); 63 | 64 | // Transform the response into our cache format 65 | const filters: ToolFilters = {}; 66 | const result = response.tasks[0].result[0]; 67 | 68 | // Process each tool's filters 69 | for (const [toolName, filterPath] of Object.entries(BusinessListingsFiltersTool.TOOL_TO_FILTER_MAP)) { 70 | if (result && result[filterPath]) { 71 | filters[toolName] = result[filterPath]; 72 | } 73 | } 74 | 75 | // Update cache 76 | BusinessListingsFiltersTool.cache = filters; 77 | BusinessListingsFiltersTool.lastFetchTime = now; 78 | return filters; 79 | } 80 | 81 | async handle(params: any): Promise<any> { 82 | try { 83 | const filters = await this.fetchAndCacheFilters(); 84 | 85 | if (!params.tool) { 86 | return this.formatResponse(filters); 87 | } 88 | 89 | const toolFilters = filters[params.tool]; 90 | if (!toolFilters) { 91 | throw new Error(`No filters found for tool: ${params.tool}`); 92 | } 93 | 94 | return this.formatResponse(toolFilters); 95 | } catch (error) { 96 | return this.formatErrorResponse(error); 97 | } 98 | } 99 | } ``` -------------------------------------------------------------------------------- /src/core/modules/serp/serp.prompt.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { PromptDefinition } from '../prompt-definition.js'; 3 | 4 | 5 | export const serpPrompts: PromptDefinition[] = [ 6 | { 7 | name: 'analyze_local_seo_differences_in_the_top_10_google_results_for_two_target_markets', 8 | title: 'Analyze local SEO differences in the top 10 Google results for two target markets.', 9 | params: { 10 | keyword: z.string().describe('The keyword to analyze'), 11 | language: z.string().describe('The language of the search results'), 12 | location1: z.string().describe('The first location to analyze'), 13 | location2: z.string().describe('The second location to analyze'), 14 | }, 15 | handler: async (params) => { 16 | return { 17 | messages: [ 18 | { 19 | role: 'user', 20 | content: { 21 | type: 'text', 22 | text: `Use the serp_organic_live_advanced API to fetch the top 10 results and SERP features for the keyword '${params.keyword}' in '${params.language}' for two locations with two separate API requests: '${params.location1}', then in '${params.location2}'. Display a unified table of the top 10 results for both locations side-by-side, with columns: Rank, Domain, Title, Snippet (shorten), URL, and Element Type (e.g., Organic, Knowledge Graph, Featured Snippet, etc.).` 23 | } 24 | } 25 | ] 26 | }; 27 | } 28 | }, 29 | { 30 | name: 'monitor_visibility_for_key_branded_searches_in_real_time', 31 | title: 'Monitor visibility for key branded searches in real-time.', 32 | params: { 33 | domain: z.string().describe('The domain to monitor'), 34 | location: z.string().describe('The location to monitor'), 35 | language: z.string().describe('The language to monitor'), 36 | }, 37 | handler: async (params) => { 38 | return { 39 | messages: [ 40 | { 41 | role: 'user', 42 | content: { 43 | type: 'text', 44 | text: `Using the real-time SERP API, check if '${params.domain}' currently ranks in the top 3 organic results or SERP features like featured snippet or knowledge graph for the branded keyword [keyword] in '${params.location}' in '${params.language}'. Indicate what competitors are showing in SERP features too if my domain is not featured.` 45 | } 46 | } 47 | ] 48 | }; 49 | } 50 | }, 51 | { 52 | name: 'generate_domain_visibility_reports_and_track_ranking_changes', 53 | title: 'Generate domain visibility reports and track ranking changes.', 54 | params: { 55 | domain: z.string().describe('The domain to analyze'), 56 | location: z.string().describe('The location to analyze'), 57 | language: z.string().describe('The language to analyze'), 58 | }, 59 | handler: async (params) => { 60 | return { 61 | messages: [ 62 | { 63 | role: 'user', 64 | content: { 65 | type: 'text', 66 | text: `Generate a domain visibility snapshot for '${params.domain}' in '${params.location}' in '${params.language}' using google_domain_rank_overview. List the estimated organic traffic, percentage of top 10 rankings, and SERP position breakdown for this week. Compare to last month's values using google_historical_rank_overview.` 67 | } 68 | } 69 | ] 70 | }; 71 | } 72 | }, 73 | ] 74 | ``` -------------------------------------------------------------------------------- /src/core/modules/backlinks/tools/backlinks-domain-pages-summary.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { DataForSEOClient } from '../../../client/dataforseo.client.js'; 3 | import { BaseTool } from '../../base.tool.js'; 4 | 5 | export class BacklinksDomainPagesSummaryTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'backlinks_domain_pages_summary'; 12 | } 13 | 14 | getDescription(): string { 15 | return "This endpoint will provide you with detailed summary data on all backlinks and related metrics for each page of the target domain or subdomain you specify. If you indicate a single page as a target, you will get comprehensive summary data on all backlinks for that page"; 16 | } 17 | 18 | getParams(): z.ZodRawShape { 19 | return { 20 | target: z.string().describe(`domain, subdomain or webpage to get backlinks for 21 | required field 22 | a domain or a subdomain should be specified without https:// and www. 23 | a page should be specified with absolute URL (including http:// or https://)`), 24 | limit: z.number().min(1).max(1000).default(10).optional().describe("the maximum number of returned anchors"), 25 | offset: z.number().min(0).optional().describe( 26 | `offset in the results array of returned anchors 27 | optional field 28 | default value: 0 29 | if you specify the 10 value, the first ten anchors in the results array will be omitted and the data will be provided for the successive anchors` 30 | ), 31 | filters: this.getFilterExpression().optional().describe( 32 | `array of results filtering parameters 33 | optional field 34 | you can add several filters at once (8 filters maximum) 35 | you should set a logical operator and, or between the conditions 36 | the following operators are supported: 37 | regex, not_regex, =, <>, in, not_in, like, not_like, ilike, not_ilike, match, not_match 38 | you can use the % operator with like and not_like to match any string of zero or more characters 39 | example: 40 | ["referring_links_types.anchors",">","1"] 41 | [["broken_pages",">","2"], 42 | "and", 43 | ["backlinks",">","10"]] 44 | 45 | [["first_seen",">","2017-10-23 11:31:45 +00:00"], 46 | "and", 47 | [["anchor","like","%seo%"],"or",["referring_domains",">","10"]]]` 48 | ), 49 | order_by: z.array(z.string()).optional().describe( 50 | `results sorting rules 51 | optional field 52 | you can use the same values as in the filters array to sort the results 53 | possible sorting types: 54 | asc – results will be sorted in the ascending order 55 | desc – results will be sorted in the descending order 56 | you should use a comma to set up a sorting type 57 | example: 58 | ["backlinks,desc"] 59 | note that you can set no more than three sorting rules in a single request 60 | you should use a comma to separate several sorting rules 61 | example: 62 | ["backlinks,desc","rank,asc"]` 63 | ), 64 | }; 65 | } 66 | 67 | async handle(params: any): Promise<any> { 68 | try { 69 | const response = await this.client.makeRequest('/v3/backlinks/domain_pages_summary/live', 'POST', [{ 70 | target: params.target, 71 | limit: params.limit, 72 | offset: params.offset, 73 | filters: this.formatFilters(params.filters), 74 | order_by: this.formatOrderBy(params.order_by), 75 | }]); 76 | return this.validateAndFormatResponse(response); 77 | } catch (error) { 78 | return this.formatErrorResponse(error); 79 | } 80 | } 81 | } ``` -------------------------------------------------------------------------------- /src/core/modules/domain-analytics/tools/whois/whois-filters.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { DataForSEOClient } from '../../../../client/dataforseo.client.js'; 3 | import { BaseTool, DataForSEOFullResponse } from '../../../base.tool.js'; 4 | 5 | interface FilterField { 6 | type: string; 7 | path: string; 8 | } 9 | 10 | interface ToolFilters { 11 | [key: string]: { 12 | [field: string]: string; 13 | }; 14 | } 15 | 16 | export class WhoisFiltersTool extends BaseTool { 17 | private static cache: ToolFilters | null = null; 18 | private static lastFetchTime: number = 0; 19 | private static readonly CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours in milliseconds 20 | 21 | // Map of tool names to their corresponding filter paths in the API response 22 | private static readonly TOOL_TO_FILTER_MAP: { [key: string]: string } = { 23 | 'domain_analytics_whois_overview': 'overview' 24 | }; 25 | 26 | constructor(private client: DataForSEOClient) { 27 | super(client); 28 | } 29 | 30 | getName(): string { 31 | return 'domain_analytics_whois_available_filters'; 32 | } 33 | 34 | getDescription(): string { 35 | return `Here you will find all the necessary information about filters that can be used with DataForSEO WHOIS API endpoints. 36 | 37 | Please, keep in mind that filters are associated with a certain object in the result array, and should be specified accordingly.`; 38 | } 39 | 40 | protected supportOnlyFullResponse(): boolean { 41 | return true; 42 | } 43 | 44 | getParams(): z.ZodRawShape { 45 | return { 46 | tool: z.string().optional().describe('The name of the tool to get filters for') 47 | }; 48 | } 49 | 50 | private async fetchAndCacheFilters(): Promise<ToolFilters> { 51 | const now = Date.now(); 52 | 53 | // Return cached data if it's still valid 54 | if (WhoisFiltersTool.cache && 55 | (now - WhoisFiltersTool.lastFetchTime) < WhoisFiltersTool.CACHE_TTL) { 56 | return WhoisFiltersTool.cache; 57 | } 58 | 59 | // Fetch fresh data 60 | const response = await this.client.makeRequest('/v3/domain_analytics/whois/available_filters', 'GET', null, true) as DataForSEOFullResponse; 61 | this.validateResponseFull(response); 62 | 63 | // Transform the response into our cache format 64 | const filters: ToolFilters = {}; 65 | const result = response.tasks[0].result[0]; 66 | 67 | // Process each tool's filters 68 | for (const [toolName, filterPath] of Object.entries(WhoisFiltersTool.TOOL_TO_FILTER_MAP)) { 69 | const pathParts = filterPath.split('.'); 70 | let current = result; 71 | 72 | // Navigate to the correct filter object 73 | for (const part of pathParts) { 74 | if (current && current[part]) { 75 | current = current[part]; 76 | } else { 77 | current = null; 78 | break; 79 | } 80 | } 81 | 82 | if (current) { 83 | filters[toolName] = current; 84 | } 85 | } 86 | 87 | // Update cache 88 | WhoisFiltersTool.cache = filters; 89 | WhoisFiltersTool.lastFetchTime = now; 90 | 91 | return filters; 92 | } 93 | 94 | async handle(params: any): Promise<any> { 95 | try { 96 | const filters = await this.fetchAndCacheFilters(); 97 | 98 | if (!params.tool) { 99 | return this.formatResponse(filters); 100 | } 101 | 102 | const toolFilters = filters[params.tool]; 103 | if (!toolFilters) { 104 | throw new Error(`No filters found for tool: ${params.tool}`); 105 | } 106 | 107 | return this.formatResponse(toolFilters); 108 | } catch (error) { 109 | return this.formatErrorResponse(error); 110 | } 111 | } 112 | } ```