This is page 2 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 -------------------------------------------------------------------------------- /src/core/modules/backlinks/tools/backlinks-referring-networks.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 BacklinksReferringNetworksTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'backlinks_referring_networks'; 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 | network_address_type: z.string().optional().default('ip').describe(`indicates the type of network to get data for 25 | optional field 26 | possible values: ip, subnet 27 | default value: ip`), 28 | limit: z.number().min(1).max(1000).default(10).optional().describe("the maximum number of returned networks"), 29 | offset: z.number().min(0).optional().describe( 30 | `offset in the results array of returned networks 31 | optional field 32 | default value: 0 33 | if you specify the 10 value, the first ten domains in the results array will be omitted and the data will be provided for the successive pages` 34 | ), 35 | filters: this.getFilterExpression().optional().describe( 36 | `array of results filtering parameters 37 | optional field 38 | you can add several filters at once (8 filters maximum) 39 | you should set a logical operator and, or between the conditions 40 | the following operators are supported: 41 | regex, not_regex, =, <>, in, not_in, like, not_like, ilike, not_ilike, match, not_match 42 | you can use the % operator with like and not_like to match any string of zero or more characters 43 | example: 44 | ["referring_pages",">","1"] 45 | [["referring_pages",">","2"], 46 | "and", 47 | ["backlinks",">","10"]] 48 | 49 | [["first_seen",">","2017-10-23 11:31:45 +00:00"], 50 | "and", 51 | [["network_address","like","194.1.%"],"or",["referring_ips",">","10"]]]` 52 | ), 53 | order_by: z.array(z.string()).optional().describe( 54 | `results sorting rules 55 | optional field 56 | you can use the same values as in the filters array to sort the results 57 | possible sorting types: 58 | asc – results will be sorted in the ascending order 59 | desc – results will be sorted in the descending order 60 | you should use a comma to set up a sorting type 61 | example: 62 | ["backlinks,desc"] 63 | note that you can set no more than three sorting rules in a single request 64 | you should use a comma to separate several sorting rules 65 | example: 66 | ["backlinks,desc","rank,asc"]` 67 | ), 68 | }; 69 | } 70 | 71 | async handle(params: any): Promise<any> { 72 | try { 73 | const response = await this.client.makeRequest('/v3/backlinks/referring_networks/live', 'POST', [{ 74 | target: params.target, 75 | limit: params.limit, 76 | offset: params.offset, 77 | filters: this.formatFilters(params.filters), 78 | order_by: this.formatOrderBy(params.order_by), 79 | network_address_type: params.network_address_type 80 | }]); 81 | return this.validateAndFormatResponse(response); 82 | } catch (error) { 83 | return this.formatErrorResponse(error); 84 | } 85 | } 86 | } ``` -------------------------------------------------------------------------------- /src/core/modules/backlinks/tools/backlinks-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 BacklinksTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'backlinks_backlinks'; 12 | } 13 | 14 | getDescription(): string { 15 | return "This endpoint will provide you with a list of backlinks and relevant data for the specified 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 | mode: z.string().default("as_is").describe(`results grouping type 25 | optional field 26 | possible grouping types: 27 | as_is – returns all backlinks 28 | one_per_domain – returns one backlink per domain 29 | one_per_anchor – returns one backlink per anchor 30 | default value: as_is`), 31 | limit: z.number().min(1).max(1000).default(10).optional().describe("the maximum number of returned backlinks"), 32 | offset: z.number().min(0).optional().describe( 33 | `offset in the results array of the returned backlinks 34 | optional field 35 | default value: 0 36 | if you specify the 10 value, the first ten backlinks in the results array will be omitted and the data will be provided for the successive backlinks` 37 | ), 38 | filters: this.getFilterExpression().optional().describe( 39 | `array of results filtering parameters 40 | optional field 41 | you can add several filters at once (8 filters maximum) 42 | you should set a logical operator and, or between the conditions 43 | the following operators are supported: 44 | =, <>, in, not_in, like, not_like, ilike, not_ilike, regex, not_regex, match, not_match 45 | you can use the % operator with like and not_like to match any string of zero or more characters 46 | example: 47 | ["rank",">","80"] 48 | [["page_from_rank",">","55"], 49 | "and", 50 | ["dofollow","=",true]] 51 | 52 | [["first_seen",">","2017-10-23 11:31:45 +00:00"], 53 | "and", 54 | [["anchor","like","%seo%"],"or",["text_pre","like","%seo%"]]]` ), 55 | order_by: z.array(z.string()).optional().describe( 56 | `results sorting rules 57 | optional field 58 | you can use the same values as in the filters array to sort the results 59 | possible sorting types: 60 | asc – results will be sorted in the ascending order 61 | desc – results will be sorted in the descending order 62 | you should use a comma to set up a sorting type 63 | example: 64 | ["rank,desc"] 65 | note that you can set no more than three sorting rules in a single request 66 | you should use a comma to separate several sorting rules 67 | example: 68 | ["domain_from_rank,desc","page_from_rank,asc"]` 69 | ), 70 | }; 71 | } 72 | 73 | async handle(params: any): Promise<any> { 74 | try { 75 | const response = await this.client.makeRequest('/v3/backlinks/backlinks/live', 'POST', [{ 76 | target: params.target, 77 | mode: params.mode, 78 | limit: params.limit, 79 | offset: params.offset, 80 | filters: this.formatFilters(params.filters), 81 | order_by: this.formatOrderBy(params.order_by) 82 | }]); 83 | return this.validateAndFormatResponse(response); 84 | } catch (error) { 85 | return this.formatErrorResponse(error); 86 | } 87 | } 88 | } ``` -------------------------------------------------------------------------------- /src/core/modules/backlinks/tools/backlinks-page-intersection.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { DataForSEOClient } from '../../../client/dataforseo.client.js'; 3 | import { BaseTool } from '../../base.tool.js'; 4 | import { mapArrayToNumberedKeys } from '../../../utils/map-array-to-numbered-keys.js'; 5 | 6 | export class BacklinksPageIntersectionTool extends BaseTool { 7 | constructor(private client: DataForSEOClient) { 8 | super(client); 9 | } 10 | 11 | getName(): string { 12 | return 'backlinks_page_intersection'; 13 | } 14 | 15 | getDescription(): string { 16 | return "This endpoint will provide you with the list of domains pointing to the specified websites. This endpoint is especially useful for creating a Link Gap feature that shows what domains link to your competitors but do not link out to your website"; 17 | } 18 | 19 | getParams(): z.ZodRawShape { 20 | return { 21 | targets: z.array(z.string()).describe(`domains, subdomains or webpages to get links for 22 | required field 23 | you can set up to 20 domains, subdomains or webpages 24 | a domain or a subdomain should be specified without https:// and www. 25 | a page should be specified with absolute URL (including http:// or https://)`), 26 | limit: z.number().min(1).max(1000).default(10).optional().describe("the maximum number of returned results"), 27 | offset: z.number().min(0).optional().describe( 28 | `offset in the array of returned results 29 | optional field 30 | default value: 0 31 | if you specify the 10 value, the first ten backlinks in the results array will be omitted and the data will be provided for the successive backlinks` 32 | ), 33 | filters: this.getFilterExpression().optional().describe( 34 | `array of results filtering parameters 35 | optional field 36 | you can add several filters at once (8 filters maximum) 37 | you should set a logical operator and, or between the conditions 38 | the following operators are supported: 39 | regex, not_regex, =, <>, in, not_in, like, not_like, ilike, not_ilike, match, not_match 40 | you can use the % operator with like and not_like to match any string of zero or more characters 41 | example: 42 | ["1.rank",">","80"] 43 | [["2.page_from_rank",">","55"], 44 | "and", 45 | ["1.original","=","true"]] 46 | 47 | [["1.first_seen",">","2017-10-23 11:31:45 +00:00"], 48 | "and", 49 | [["1.anchor","like","%seo%"],"or",["1.text_pre","not_like","%seo%"]]]` 50 | ), 51 | order_by: z.array(z.string()).optional().describe( 52 | `results sorting rules 53 | optional field 54 | you can use the same values as in the filters array to sort the results 55 | possible sorting types: 56 | asc – results will be sorted in the ascending order 57 | desc – results will be sorted in the descending order 58 | you should use a comma to set up a sorting type 59 | example: 60 | ["rank,desc"] 61 | note that you can set no more than three sorting rules in a single request 62 | you should use a comma to separate several sorting rules 63 | example: 64 | ["domain_from_rank,desc","page_from_rank,asc"]` 65 | ), 66 | }; 67 | } 68 | 69 | async handle(params: any): Promise<any> { 70 | try { 71 | const response = await this.client.makeRequest('/v3/backlinks/page_intersection/live', 'POST', [{ 72 | targets: mapArrayToNumberedKeys(params.targets), 73 | limit: params.limit, 74 | offset: params.offset, 75 | filters: this.formatFilters(params.filters), 76 | order_by: this.formatOrderBy(params.order_by), 77 | }]); 78 | return this.validateAndFormatResponse(response); 79 | } catch (error) { 80 | return this.formatErrorResponse(error); 81 | } 82 | } 83 | } ``` -------------------------------------------------------------------------------- /src/core/modules/backlinks/tools/backlinks-domain-intersection.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { DataForSEOClient } from '../../../client/dataforseo.client.js'; 3 | import { BaseTool } from '../../base.tool.js'; 4 | import { mapArrayToNumberedKeys } from '../../../utils/map-array-to-numbered-keys.js'; 5 | 6 | export class BacklinksDomainIntersectionTool extends BaseTool { 7 | constructor(private client: DataForSEOClient) { 8 | super(client); 9 | } 10 | 11 | getName(): string { 12 | return 'backlinks_domain_intersection'; 13 | } 14 | 15 | getDescription(): string { 16 | return "This endpoint will provide you with the list of domains pointing to the specified websites. This endpoint is especially useful for creating a Link Gap feature that shows what domains link to your competitors but do not link out to your website"; 17 | } 18 | 19 | getParams(): z.ZodRawShape { 20 | return { 21 | targets: z.array(z.string()).describe(`domains, subdomains or webpages to get links for 22 | required field 23 | you can set up to 20 domains, subdomains or webpages 24 | a domain or a subdomain should be specified without https:// and www. 25 | a page should be specified with absolute URL (including http:// or https://)`), 26 | limit: z.number().min(1).max(1000).default(10).optional().describe("the maximum number of returned results"), 27 | offset: z.number().min(0).optional().describe( 28 | `offset in the array of returned results 29 | optional field 30 | default value: 0 31 | if you specify the 10 value, the first ten backlinks in the results array will be omitted and the data will be provided for the successive backlinks` 32 | ), 33 | filters: this.getFilterExpression().optional().describe( 34 | `array of results filtering parameters 35 | optional field 36 | you can add several filters at once (8 filters maximum) 37 | you should set a logical operator and, or between the conditions 38 | the following operators are supported: 39 | regex, not_regex, =, <>, in, not_in, like, not_like, ilike, not_ilike, match, not_match 40 | you can use the % operator with like and not_like to match any string of zero or more characters 41 | example: 42 | ["1.internal_links_count",">","1"] 43 | [["2.referring_pages",">","2"], 44 | "and", 45 | ["1.backlinks",">","10"]] 46 | 47 | [["1.first_seen",">","2017-10-23 11:31:45 +00:00"], 48 | "and", 49 | [["2.target","like","%dataforseo.com%"],"or",["1.referring_domains",">","10"]]]` 50 | ), 51 | order_by: z.array(z.string()).optional().describe( 52 | `results sorting rules 53 | optional field 54 | you can use the same values as in the filters array to sort the results 55 | possible sorting types: 56 | asc – results will be sorted in the ascending order 57 | desc – results will be sorted in the descending order 58 | you should use a comma to set up a sorting type 59 | example: 60 | ["backlinks,desc"] 61 | note that you can set no more than three sorting rules in a single request 62 | you should use a comma to separate several sorting rules 63 | example: 64 | ["backlinks,desc","rank,asc"]` 65 | ), 66 | }; 67 | } 68 | 69 | async handle(params: any): Promise<any> { 70 | try { 71 | const response = await this.client.makeRequest('/v3/backlinks/domain_intersection/live', 'POST', [{ 72 | targets: mapArrayToNumberedKeys(params.targets), 73 | limit: params.limit, 74 | offset: params.offset, 75 | filters: this.formatFilters(params.filters), 76 | order_by: this.formatOrderBy(params.order_by), 77 | }]); 78 | return this.validateAndFormatResponse(response); 79 | } catch (error) { 80 | return this.formatErrorResponse(error); 81 | } 82 | } 83 | } ``` -------------------------------------------------------------------------------- /src/core/modules/domain-analytics/tools/technologies/domain-technologies-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 DomainTechnologiesFiltersTool 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_technologies_domains_by_technology': 'domains_by_technology', 24 | 'domain_analytics_technologies_aggregation_technologies': 'aggregation_technologies', 25 | 'domain_analytics_technologies_technologies_summary': 'technologies_summary', 26 | 'domain_analytics_technologies_domains_by_html_terms': 'domains_by_html_terms' 27 | }; 28 | 29 | constructor(private client: DataForSEOClient) { 30 | super(client); 31 | } 32 | 33 | getName(): string { 34 | return 'domain_analytics_technologies_available_filters'; 35 | } 36 | 37 | getDescription(): string { 38 | return `Here you will find all the necessary information about filters that can be used with DataForSEO Technologies API endpoints. 39 | 40 | Please, keep in mind that filters are associated with a certain object in the result array, and should be specified accordingly.`; 41 | } 42 | 43 | protected supportOnlyFullResponse(): boolean { 44 | return true; 45 | } 46 | 47 | getParams(): z.ZodRawShape { 48 | return { 49 | tool: z.string().optional().describe('The name of the tool to get filters for') 50 | }; 51 | } 52 | 53 | private async fetchAndCacheFilters(): Promise<ToolFilters> { 54 | const now = Date.now(); 55 | 56 | // Return cached data if it's still valid 57 | if (DomainTechnologiesFiltersTool.cache && 58 | (now - DomainTechnologiesFiltersTool.lastFetchTime) < DomainTechnologiesFiltersTool.CACHE_TTL) { 59 | return DomainTechnologiesFiltersTool.cache; 60 | } 61 | 62 | // Fetch fresh data 63 | const response = await this.client.makeRequest('/v3/domain_analytics/technologies/available_filters', 'GET', null, true) as DataForSEOFullResponse; 64 | this.validateResponseFull(response); 65 | 66 | // Transform the response into our cache format 67 | const filters: ToolFilters = {}; 68 | const result = response.tasks[0].result[0]; 69 | 70 | // Process each tool's filters 71 | for (const [toolName, filterPath] of Object.entries(DomainTechnologiesFiltersTool.TOOL_TO_FILTER_MAP)) { 72 | const pathParts = filterPath.split('.'); 73 | let current = result; 74 | 75 | // Navigate to the correct filter object 76 | for (const part of pathParts) { 77 | if (current && current[part]) { 78 | current = current[part]; 79 | } else { 80 | current = null; 81 | break; 82 | } 83 | } 84 | 85 | if (current) { 86 | filters[toolName] = current; 87 | } 88 | } 89 | 90 | // Update cache 91 | DomainTechnologiesFiltersTool.cache = filters; 92 | DomainTechnologiesFiltersTool.lastFetchTime = now; 93 | 94 | return filters; 95 | } 96 | 97 | async handle(params: any): Promise<any> { 98 | try { 99 | const filters = await this.fetchAndCacheFilters(); 100 | 101 | if (!params.tool) { 102 | return this.formatResponse(filters); 103 | } 104 | 105 | const toolFilters = filters[params.tool]; 106 | if (!toolFilters) { 107 | throw new Error(`No filters found for tool: ${params.tool}`); 108 | } 109 | 110 | return this.formatResponse(toolFilters); 111 | } catch (error) { 112 | return this.formatErrorResponse(error); 113 | } 114 | } 115 | } ``` -------------------------------------------------------------------------------- /src/core/modules/backlinks/backlinks.prompt.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { PromptDefinition } from '../prompt-definition.js'; 3 | 4 | 5 | export const backlinksPrompts: PromptDefinition[] = [ 6 | { 7 | name: 'discover_your_strongest_backlinks_for_authority_building', 8 | title: 'Discover your strongest backlinks for authority building.', 9 | params: { 10 | domain: z.string().describe('The domain to find for'), 11 | }, 12 | handler: async (params) => { 13 | return { 14 | messages: [ 15 | { 16 | role: 'user', 17 | content: { 18 | type: 'text', 19 | text: `Identify the top 10 highest-authority backlinks to '${params.domain}', grouped by referring domain. Include backlink type, anchor text, and target page.` 20 | } 21 | } 22 | ] 23 | }; 24 | } 25 | }, 26 | { 27 | name: 'see_which_blog_content_earns_you_the_most_backlinks', 28 | title: 'See which blog content earns you the most backlinks.', 29 | params: { 30 | domain: z.string().describe('The domain to analyze'), 31 | }, 32 | handler: async (params) => { 33 | return { 34 | messages: [ 35 | { 36 | role: 'user', 37 | content:{ 38 | type: 'text', 39 | text: `Show me which blog posts on '${params.domain}' attract the most backlinks. List the top 5 by backlink count, and include title, referring domains, and anchor types.` 40 | } 41 | } 42 | ] 43 | } 44 | } 45 | }, 46 | { 47 | name: 'find_new_link_opportunities_from_competitor_backlinks', 48 | title: 'Find new link opportunities from competitor backlinks.', 49 | params: { 50 | my_domain: z.string().describe('Your domain to compare against competitors'), 51 | competitor_1: z.string().describe('First competitor domain'), 52 | competitor_2: z.string().describe('Second competitor domain'), 53 | }, 54 | handler: async (params) => { 55 | return { 56 | messages: [ 57 | { 58 | role: 'user', 59 | content:{ 60 | type: 'text', 61 | text: `Which websites link to my competitors but not to '${params.my_domain}'? Use '${params.competitor_1}' and '${params.competitor_2}'. Return 15 domains I should target for outreach.` 62 | } 63 | } 64 | ] 65 | } 66 | } 67 | }, 68 | { 69 | name: 'locate_broken_or_redirected_pages_that_waste_valuable_links', 70 | title: 'Locate broken or redirected pages that waste valuable links.', 71 | params: { 72 | domain: z.string().describe('The domain to analyze'), 73 | backlinks_count: z.number().default(30).describe('Minimum number of backlinks to consider a page valuable'), 74 | }, 75 | handler: async (params) => { 76 | return { 77 | messages: [ 78 | { 79 | role: 'user', 80 | content:{ 81 | type: 'text', 82 | text: `Find internal pages on '${params.domain}' that have over ${params.backlinks_count} backlinks but are 404 or redirected. Return URL, status code, backlink count, and top referring domains.` 83 | } 84 | } 85 | ] 86 | } 87 | } 88 | }, 89 | { 90 | name: 'benchmark_backlink_gaps_between_you_and_a_competitor', 91 | title: 'Benchmark backlink gaps between you and a competitor.', 92 | params: { 93 | my_domain: z.string().describe('Your domain to compare against a competitor'), 94 | competitor: z.string().describe('Competitor domain to analyze'), 95 | }, 96 | handler: async (params) => { 97 | return { 98 | messages: [ 99 | { 100 | role: 'user', 101 | content:{ 102 | type: 'text', 103 | text: `Compare backlinks between '${params.my_domain}' and competitor '${params.competitor}'. Show 10 domains linking only to the competitor domain. Include domain authority and link count.` 104 | } 105 | } 106 | ] 107 | } 108 | } 109 | } 110 | ] 111 | ``` -------------------------------------------------------------------------------- /src/core/modules/backlinks/tools/backlinks-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 BacklinksFiltersTool 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 | 'backlinks_content_duplicates': 'content_duplicates', 24 | 'backlinks_backlinks': 'backlinks', 25 | 'backlinks_domain_pages': 'domain_pages', 26 | 'backlinks_anchors': 'anchors', 27 | 'backlinks_referring_domains': 'referring_domains', 28 | 'backlinks_domain_intersection': 'domain_intersection', 29 | 'backlinks_page_intersection': 'page_intersection', 30 | 'backlinks_referring_networks': 'referring_networks', 31 | 'backlinks_domain_pages_summary': 'domain_pages_summary', 32 | 'backlinks_competitors': 'competitors' 33 | }; 34 | 35 | constructor(private client: DataForSEOClient) { 36 | super(client); 37 | } 38 | 39 | getName(): string { 40 | return 'backlinks_available_filters'; 41 | } 42 | 43 | getDescription(): string { 44 | return `Here you will find all the necessary information about filters that can be used with DataForSEO Backlinks API endpoints. 45 | 46 | Please, keep in mind that filters are associated with a certain object in the result array, and should be specified accordingly.`; 47 | } 48 | 49 | protected supportOnlyFullResponse(): boolean { 50 | return true; 51 | } 52 | 53 | getParams(): z.ZodRawShape { 54 | return { 55 | tool: z.string().optional().describe('The name of the tool to get filters for') 56 | }; 57 | } 58 | 59 | private async fetchAndCacheFilters(): Promise<ToolFilters> { 60 | const now = Date.now(); 61 | 62 | // Return cached data if it's still valid 63 | if (BacklinksFiltersTool.cache && 64 | (now - BacklinksFiltersTool.lastFetchTime) < BacklinksFiltersTool.CACHE_TTL) { 65 | return BacklinksFiltersTool.cache; 66 | } 67 | 68 | // Fetch fresh data 69 | const response = await this.client.makeRequest('/v3/backlinks/available_filters', 'GET', null, true) as DataForSEOFullResponse; 70 | this.validateResponseFull(response); 71 | 72 | // Transform the response into our cache format 73 | const filters: ToolFilters = {}; 74 | const result = response.tasks[0].result[0]; 75 | 76 | // Process each tool's filters 77 | for (const [toolName, filterPath] of Object.entries(BacklinksFiltersTool.TOOL_TO_FILTER_MAP)) { 78 | const pathParts = filterPath.split('.'); 79 | let current = result; 80 | 81 | // Navigate to the correct filter object 82 | for (const part of pathParts) { 83 | if (current && current[part]) { 84 | current = current[part]; 85 | } else { 86 | current = null; 87 | break; 88 | } 89 | } 90 | 91 | if (current) { 92 | filters[toolName] = current; 93 | } 94 | } 95 | 96 | // Update cache 97 | BacklinksFiltersTool.cache = filters; 98 | BacklinksFiltersTool.lastFetchTime = now; 99 | 100 | return filters; 101 | } 102 | 103 | async handle(params: any): Promise<any> { 104 | try { 105 | const filters = await this.fetchAndCacheFilters(); 106 | 107 | if (!params.tool) { 108 | return this.formatResponse(filters); 109 | } 110 | 111 | const toolFilters = filters[params.tool]; 112 | if (!toolFilters) { 113 | throw new Error(`No filters found for tool: ${params.tool}`); 114 | } 115 | 116 | return this.formatResponse(toolFilters); 117 | } catch (error) { 118 | return this.formatErrorResponse(error); 119 | } 120 | } 121 | } ``` -------------------------------------------------------------------------------- /src/core/modules/content-analysis/tools/content-analysis-phrase-trends.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { any, z } from 'zod'; 2 | import { BaseTool } from '../../base.tool.js'; 3 | import { DataForSEOClient } from '../../../client/dataforseo.client.js'; 4 | 5 | export class ContentAnalysisPhraseTrendsTool extends BaseTool { 6 | constructor(dataForSEOClient: DataForSEOClient) { 7 | super(dataForSEOClient); 8 | } 9 | 10 | getName(): string { 11 | return 'content_analysis_phrase_trends'; 12 | } 13 | 14 | getDescription(): string { 15 | return `This endpoint will provide you with data on all citations of the target keyword for the indicated date range`; 16 | } 17 | 18 | getParams(): z.ZodRawShape { 19 | return { 20 | keyword: z.string().describe(`target keyword 21 | Note: to match an exact phrase instead of a stand-alone keyword, use double quotes and backslashes;`), 22 | keyword_fields: z.object({ 23 | title: z.string().optional(), 24 | main_title: z.string().optional(), 25 | previous_title: z.string().optional(), 26 | snippet: z.string().optional() 27 | }).optional().describe( 28 | `target keyword fields and target keywords 29 | use this parameter to filter the dataset by keywords that certain fields should contain; 30 | you can indicate several fields; 31 | Note: to match an exact phrase instead of a stand-alone keyword, use double quotes and backslashes; 32 | example: 33 | { 34 | "snippet": "\\"logitech mouse\\"", 35 | "main_title": "sale" 36 | }` 37 | ), 38 | page_type: z.array(z.enum(['ecommerce','news','blogs', 'message-boards','organization'])).optional().describe(`target page types`), 39 | initial_dataset_filters: this.getFilterExpression().optional().describe( 40 | `initial dataset filtering parameters 41 | initial filtering parameters that apply to fields in the Search endpoint; 42 | you can add several filters at once (8 filters maximum); 43 | you should set a logical operator and, or between the conditions; 44 | the following operators are supported: 45 | regex, not_regex, <, <=, >, >=, =, <>, in, not_in, like,not_like, has, has_not, match, not_match 46 | you can use the % operator with like and not_like to match any string of zero or more characters; 47 | example: 48 | ["domain","<>", "logitech.com"] 49 | [["domain","<>","logitech.com"],"and",["content_info.connotation_types.negative",">",1000]] 50 | 51 | [["domain","<>","logitech.com"]], 52 | "and", 53 | [["content_info.connotation_types.negative",">",1000], 54 | "or", 55 | ["content_info.text_category","has",10994]]` 56 | ), 57 | date_from: z.string().describe(`starting date of the time range 58 | date format: "yyyy-mm-dd"`), 59 | date_to: z.string().describe(`ending date of the time range 60 | date format: "yyyy-mm-dd"`).optional(), 61 | date_group: z.enum(['day', 'week', 'month']).default('month').describe(`date grouping type`).optional(), 62 | internal_list_limit: z.number().min(1).max(20).default(1) 63 | .describe( 64 | `maximum number of elements within internal arrays 65 | you can use this field to limit the number of elements within the following arrays`) 66 | .optional(), 67 | }; 68 | } 69 | 70 | async handle(params: any): Promise<any> { 71 | try { 72 | const response = await this.dataForSEOClient.makeRequest('/v3/content_analysis/phrase_trends/live', 'POST', [{ 73 | keyword: params.keyword, 74 | keyword_fields: params.keyword_fields, 75 | page_type: params.page_type, 76 | initial_dataset_filters: this.formatFilters(params.initial_dataset_filters), 77 | date_from: params.date_from, 78 | date_to: params.date_to, 79 | date_group: params.date_group, 80 | internal_list_limit: params.internal_list_limit 81 | }]); 82 | return this.validateAndFormatResponse(response); 83 | } catch (error) { 84 | return this.formatErrorResponse(error); 85 | } 86 | } 87 | } ``` -------------------------------------------------------------------------------- /src/core/modules/content-analysis/tools/content-analysis-search.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { any, z } from 'zod'; 2 | import { BaseTool } from '../../base.tool.js'; 3 | import { DataForSEOClient } from '../../../client/dataforseo.client.js'; 4 | 5 | export class ContentAnalysisSearchTool extends BaseTool { 6 | constructor(dataForSEOClient: DataForSEOClient) { 7 | super(dataForSEOClient); 8 | } 9 | 10 | getName(): string { 11 | return 'content_analysis_search'; 12 | } 13 | 14 | getDescription(): string { 15 | return `This endpoint will provide you with detailed citation data available for the target keyword`; 16 | } 17 | 18 | getParams(): z.ZodRawShape { 19 | return { 20 | keyword: z.string().describe(`target keyword 21 | Note: to match an exact phrase instead of a stand-alone keyword, use double quotes and backslashes;`), 22 | keyword_fields: z.object({ 23 | title: z.string().optional(), 24 | main_title: z.string().optional(), 25 | previous_title: z.string().optional(), 26 | snippet: z.string().optional() 27 | }).optional().describe( 28 | `target keyword fields and target keywords 29 | use this parameter to filter the dataset by keywords that certain fields should contain; 30 | you can indicate several fields; 31 | Note: to match an exact phrase instead of a stand-alone keyword, use double quotes and backslashes; 32 | example: 33 | { 34 | "snippet": "\\"logitech mouse\\"", 35 | "main_title": "sale" 36 | }` 37 | ), 38 | page_type: z.array(z.enum(['ecommerce','news','blogs', 'message-boards','organization'])).optional().describe(`target page types`), 39 | search_mode: z.enum(['as_is', 'one_per_domain']).optional().describe(`results grouping type`), 40 | limit: z.number().min(1).max(1000).default(10).describe(`maximum number of results to return`), 41 | offset: z.number().min(0).default(0).describe(`offset in the results array of returned keywords`), 42 | filters: this.getFilterExpression().optional().describe( 43 | `array of results filtering parameters 44 | optional field 45 | you can add several filters at once (8 filters maximum) 46 | you should set a logical operator and, or between the conditions 47 | the following operators are supported: 48 | regex, not_regex, <, <=, >, >=, =, <>, in, not_in, like,not_like, match, not_match 49 | you can use the % operator with like and not_like to match any string of zero or more characters 50 | example: 51 | ["country","=", "US"] 52 | [["domain_rank",">",800],"and",["content_info.connotation_types.negative",">",0.9]] 53 | 54 | [["domain_rank",">",800], 55 | "and", 56 | [["page_types","has","ecommerce"], 57 | "or", 58 | ["content_info.text_category","has",10994]]` 59 | ), 60 | order_by: z.array(z.string()).optional().describe( 61 | `results sorting rules 62 | optional field 63 | you can use the same values as in the filters array to sort the results 64 | possible sorting types: 65 | asc – results will be sorted in the ascending order 66 | desc – results will be sorted in the descending order 67 | you should use a comma to set up a sorting type 68 | example: 69 | ["content_info.sentiment_connotations.anger,desc"] 70 | default rule: 71 | ["content_info.sentiment_connotations.anger,desc"] 72 | note that you can set no more than three sorting rules in a single request 73 | you should use a comma to separate several sorting rules 74 | example: 75 | ["content_info.sentiment_connotations.anger,desc","keyword_data.keyword_info.cpc,desc"]`, 76 | ), }; 77 | } 78 | 79 | async handle(params: any): Promise<any> { 80 | try { 81 | const response = await this.dataForSEOClient.makeRequest('/v3/content_analysis/search/live', 'POST', [{ 82 | keyword: params.keyword, 83 | page_type: params.page_type, 84 | search_mode: params.search_mode, 85 | limit: params.limit, 86 | offset: params.offset, 87 | filters: this.formatFilters(params.filters), 88 | order_by: this.formatOrderBy(params.order_by), 89 | }]); 90 | return this.validateAndFormatResponse(response); 91 | } catch (error) { 92 | return this.formatErrorResponse(error); 93 | } 94 | } 95 | } ``` -------------------------------------------------------------------------------- /src/core/modules/dataforseo-labs/tools/google/market-analysis/google-top-searches.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 GoogleTopSearchesTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'dataforseo_labs_google_top_searches'; 12 | } 13 | 14 | getDescription(): string { 15 | return `The Top Searches endpoint of DataForSEO Labs API can provide you with over 7 billion keywords from the DataForSEO Keyword Database. Each keyword in the API response is provided with a set of relevant keyword data with Google Ads metrics`; 16 | } 17 | 18 | getParams(): z.ZodRawShape { 19 | return { 20 | location_name: z.string().default("United States").describe(`full name of the location 21 | required field 22 | only in format "Country" (not "City" or "Region") 23 | example: 24 | 'United Kingdom', 'United States', 'Canada'`), 25 | language_code: z.string().default("en").describe( 26 | `language code 27 | required field 28 | example: 29 | en`), 30 | limit: z.number().min(1).max(1000).default(10).optional().describe("Maximum number of keywords to return"), 31 | offset: z.number().min(0).optional().describe( 32 | `offset in the results array of returned keywords 33 | optional field 34 | default value: 0 35 | if you specify the 10 value, the first ten keywords in the results array will be omitted and the data will be provided for the successive keywords` 36 | ), 37 | filters: this.getFilterExpression().optional().describe( 38 | `you can add several filters at once (8 filters maximum) 39 | you should set a logical operator and, or between the conditions 40 | the following operators are supported: 41 | regex, not_regex, <, <=, >, >=, =, <>, in, not_in, match, not_match, ilike, not_ilike, like, not_like 42 | you can use the % operator with like and not_like, as well as ilike and not_ilike to match any string of zero or more characters 43 | merge operator must be a string and connect two other arrays, availible values: or, and. 44 | example: 45 | ["keyword_info.search_volume",">",0] 46 | [["keyword_info.search_volume","in",[0,1000]], 47 | "and", 48 | ["keyword_info.competition_level","=","LOW"]][["keyword_info.search_volume",">",100], 49 | "and", 50 | [["keyword_info.cpc","<",0.5], 51 | "or", 52 | ["keyword_info.high_top_of_page_bid","<=",0.5]]]` 53 | ), 54 | order_by: z.array(z.string()).optional().describe( 55 | `resuresults sorting rules 56 | optional field 57 | you can use the same values as in the filters array to sort the results 58 | possible sorting types: 59 | asc – results will be sorted in the ascending order 60 | desc – results will be sorted in the descending order 61 | you should use a comma to set up a sorting type 62 | example: 63 | ["keyword_info.competition,desc"] 64 | default rule: 65 | ["keyword_info.search_volume,desc"] 66 | note that you can set no more than three sorting rules in a single request 67 | you should use a comma to separate several sorting rules 68 | example: 69 | ["keyword_info.search_volume,desc","keyword_info.cpc,desc"]` 70 | ), 71 | include_clickstream_data: z.boolean().optional().default(false).describe( 72 | `Include or exclude data from clickstream-based metrics in the result`) 73 | }; 74 | } 75 | 76 | async handle(params: any): Promise<any> { 77 | try { 78 | const response = await this.client.makeRequest('/v3/dataforseo_labs/google/top_searches/live', 'POST', [{ 79 | location_name: params.location_name, 80 | language_code: params.language_code, 81 | limit: params.limit, 82 | offset: params.offset, 83 | filters: this.formatFilters(params.filters), 84 | order_by: this.formatOrderBy(params.order_by), 85 | include_clickstream_data: params.include_clickstream_data 86 | }]); 87 | return this.validateAndFormatResponse(response); 88 | } catch (error) { 89 | return this.formatErrorResponse(error); 90 | } 91 | } 92 | } ``` -------------------------------------------------------------------------------- /src/core/modules/dataforseo-labs/tools/google/keyword-research/google-keywords-for-site.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 GoogleKeywordsForSiteTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'dataforseo_labs_google_keywords_for_site'; 12 | } 13 | 14 | getDescription(): string { 15 | return `The Keywords For Site endpoint will provide you with a list of keywords relevant to the target domain. Each keyword is supplied with relevant, search volume data for the last month, cost-per-click, competition`; 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 | limit: z.number().min(1).max(1000).default(10).optional().describe("Maximum number of keywords to return"), 32 | offset: z.number().min(0).optional().describe( 33 | `offset in the results array of returned keywords 34 | optional field 35 | default value: 0 36 | if you specify the 10 value, the first ten keywords in the results array will be omitted and the data will be provided for the successive keywords` 37 | ), 38 | filters: this.getFilterExpression().optional().describe( 39 | `you can add several filters at once (8 filters maximum) 40 | you should set a logical operator and, or between the conditions 41 | the following operators are supported: 42 | regex, not_regex, <, <=, >, >=, =, <>, in, not_in, match, not_match, ilike, not_ilike, like, not_like 43 | you can use the % operator with like and not_like, as well as ilike and not_ilike to match any string of zero or more characters 44 | merge operator must be a string and connect two other arrays, availible values: or, and. 45 | example: 46 | ["keyword_info.search_volume",">",0] 47 | [["keyword_info.search_volume","in",[0,1000]], 48 | "and", 49 | ["keyword_info.competition_level","=","LOW"]][["keyword_info.search_volume",">",100], 50 | "and", 51 | [["keyword_info.cpc","<",0.5], 52 | "or", 53 | ["keyword_info.high_top_of_page_bid","<=",0.5]]]` 54 | ), 55 | order_by: z.array(z.string()).optional().describe( 56 | `results sorting rules 57 | optional field 58 | you can use the same values as in the filters array to sort the results 59 | possible sorting types: 60 | asc – results will be sorted in the ascending order 61 | desc – results will be sorted in the descending order 62 | you should use a comma to set up a sorting parameter 63 | default rule: 64 | ["relevance,desc"] 65 | example: 66 | ["relevance,desc","keyword_info.search_volume,desc"]`, 67 | ), 68 | include_subdomains: z.boolean().optional().describe("Include keywords from subdomains"), 69 | include_clickstream_data: z.boolean().optional().default(false).describe( 70 | `Include or exclude data from clickstream-based metrics in the result`) 71 | }; 72 | } 73 | 74 | async handle(params: any): Promise<any> { 75 | try { 76 | const response = await this.client.makeRequest('/v3/dataforseo_labs/google/keywords_for_site/live', 'POST', [{ 77 | target: params.target, 78 | location_name: params.location_name, 79 | language_code: params.language_code, 80 | limit: params.limit, 81 | offset: params.offset, 82 | filters: this.formatFilters(params.filters), 83 | order_by: this.formatOrderBy(params.order_by), 84 | include_subdomains: params.include_subdomains, 85 | include_clickstream_data: params.include_clickstream_data 86 | }]); 87 | return this.validateAndFormatResponse(response); 88 | } catch (error) { 89 | return this.formatErrorResponse(error); 90 | } 91 | } 92 | } ``` -------------------------------------------------------------------------------- /src/worker/index-worker.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { McpAgent } from "agents/mcp"; 2 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 3 | import { z } from 'zod'; 4 | import { DataForSEOClient, DataForSEOConfig } from '../core/client/dataforseo.client.js'; 5 | import { EnabledModulesSchema } from '../core/config/modules.config.js'; 6 | import { BaseModule, ToolDefinition } from '../core/modules/base.module.js'; 7 | import { ModuleLoaderService } from '../core/utils/module-loader.js'; 8 | import { version, name } from './version.worker.js'; 9 | 10 | /** 11 | * DataForSEO MCP Server for Cloudflare Workers 12 | * 13 | * This server provides MCP (Model Context Protocol) access to DataForSEO APIs 14 | * through a Cloudflare Worker runtime using the agents/mcp pattern. 15 | */ 16 | 17 | // Server metadata 18 | const SERVER_NAME = `${name} (Worker)`; 19 | const SERVER_VERSION = version; 20 | globalThis.__PACKAGE_VERSION__ = version; 21 | globalThis.__PACKAGE_NAME__ = name; 22 | /** 23 | * DataForSEO MCP Agent for Cloudflare Workers 24 | */ 25 | export class DataForSEOMcpAgent extends McpAgent { 26 | server = new McpServer({ 27 | name: SERVER_NAME, 28 | version: SERVER_VERSION, 29 | }); 30 | 31 | constructor(ctx: DurableObjectState, protected env: Env){ 32 | super(ctx, env); 33 | } 34 | 35 | async init() { 36 | const workerEnv = this.env || (globalThis as any).workerEnv; 37 | if (!workerEnv) { 38 | throw new Error(`Worker environment not available`); 39 | } 40 | 41 | // Initialize DataForSEO client 42 | const dataForSEOConfig: DataForSEOConfig = { 43 | username: workerEnv.DATAFORSEO_USERNAME || "", 44 | password: workerEnv.DATAFORSEO_PASSWORD || "", 45 | }; 46 | 47 | const dataForSEOClient = new DataForSEOClient(dataForSEOConfig); 48 | 49 | // Parse enabled modules from environment 50 | const enabledModules = EnabledModulesSchema.parse(workerEnv.ENABLED_MODULES); 51 | 52 | // Initialize and load modules 53 | const modules: BaseModule[] = ModuleLoaderService.loadModules(dataForSEOClient, enabledModules); 54 | 55 | // Register tools from all modules 56 | modules.forEach(module => { 57 | const tools = module.getTools(); 58 | Object.entries(tools).forEach(([name, tool]) => { 59 | const typedTool = tool as ToolDefinition; 60 | const schema = z.object(typedTool.params); 61 | this.server.tool( 62 | name, 63 | schema.shape, 64 | typedTool.handler 65 | ); 66 | }); 67 | }); 68 | } 69 | } 70 | 71 | /** 72 | * Creates a JSON-RPC error response 73 | */ 74 | function createErrorResponse(code: number, message: string): Response { 75 | return new Response(JSON.stringify({ 76 | jsonrpc: "2.0", 77 | error: { code, message }, 78 | id: null 79 | }), { 80 | status: code === -32001 ? 401 : 400, 81 | headers: { 'Content-Type': 'application/json' } 82 | }); 83 | } 84 | 85 | export default { 86 | async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> { 87 | const url = new URL(request.url); 88 | 89 | // Store environment in global context for McpAgent access 90 | (globalThis as any).workerEnv = env; 91 | 92 | // Health check endpoint 93 | if (url.pathname === '/health' && request.method === 'GET') { 94 | return new Response(JSON.stringify({ 95 | status: 'healthy', 96 | server: SERVER_NAME, 97 | version: SERVER_VERSION, 98 | timestamp: new Date().toISOString() 99 | }), { 100 | headers: { 'Content-Type': 'application/json' } 101 | }); 102 | } 103 | // Check if credentials are configured 104 | if (!env.DATAFORSEO_USERNAME || !env.DATAFORSEO_PASSWORD) { 105 | if (['/mcp','/http', '/sse', '/messages','/sse/message'].includes(url.pathname)) { 106 | return createErrorResponse(-32001, "DataForSEO credentials not configured in worker environment variables"); 107 | } 108 | } 109 | // MCP endpoints using McpAgent pattern 110 | if (url.pathname === "/sse" || url.pathname === "/sse/message") { 111 | return DataForSEOMcpAgent.serveSSE("/sse").fetch(request, env, ctx); 112 | } 113 | 114 | if (url.pathname === "/mcp" || url.pathname == '/http') { 115 | return DataForSEOMcpAgent.serve("/mcp").fetch(request, env, ctx); 116 | } 117 | 118 | return new Response("Not found", { status: 404 }); 119 | }, 120 | }; ``` -------------------------------------------------------------------------------- /src/core/modules/backlinks/tools/backlinks-competitors.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 BacklinksCompetitorsTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'backlinks_competitors'; 12 | } 13 | 14 | getDescription(): string { 15 | return "This endpoint will provide you with a list of competitors that share some part of the backlink profile with a target website, along with a number of backlink intersections and the rank of every competing website"; 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 domains"), 25 | offset: z.number().min(0).optional().describe( 26 | `offset in the results array of returned networks 27 | optional field 28 | default value: 0 29 | if you specify the 10 value, the first ten domains 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 | ["rank",">","100"] 41 | [["target","like","%forbes%"], 42 | "and", 43 | [["rank",">","100"],"or",["intersections",">","5"]]]` 44 | ), 45 | order_by: z.array(z.string()).optional().describe( 46 | `results sorting rules 47 | optional field 48 | you can use the same values as in the filters array to sort the results 49 | possible sorting types: 50 | asc – results will be sorted in the ascending order 51 | desc – results will be sorted in the descending order 52 | you should use a comma to set up a sorting type 53 | example: 54 | ["rank,desc"] 55 | note that you can set no more than three sorting rules in a single request 56 | you should use a comma to separate several sorting rules 57 | example: 58 | ["intersections,desc","rank,asc"]` 59 | ), 60 | main_domain: z.boolean().optional().describe(`indicates if only main domain of the target will be included in the search 61 | if set to true, only the main domain will be included in search`).default(true), 62 | exclude_large_domains: z.boolean().optional().describe(`indicates whether large domain will appear in results 63 | if set to true, the results from the large domain (google.com, amazon.com, etc.) will be omitted`).default(true), 64 | exclude_internal_backlinks: z.boolean().optional().describe(`indicates if internal backlinks from subdomains to the target will be excluded from the results 65 | if set to true, the results will not include data on internal backlinks from subdomains of the same domain as target 66 | if set to false, internal links will be included in the results`).default(true) 67 | }; 68 | } 69 | 70 | async handle(params: any): Promise<any> { 71 | try { 72 | const response = await this.client.makeRequest('/v3/backlinks/competitors/live', 'POST', [{ 73 | target: params.target, 74 | limit: params.limit, 75 | offset: params.offset, 76 | filters: this.formatFilters(params.filters), 77 | order_by: this.formatOrderBy(params.order_by), 78 | main_domain: params.main_domain, 79 | exclude_large_domains: params.exclude_large_domains, 80 | exclude_internal_backlinks: params.exclude_internal_backlinks 81 | }]); 82 | return this.validateAndFormatResponse(response); 83 | 84 | } catch (error) { 85 | return this.formatErrorResponse(error); 86 | } 87 | } 88 | } ``` -------------------------------------------------------------------------------- /src/core/modules/keywords-data/tools/google-trends/google-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 GoogleTrendsExploreTool extends BaseTool { 6 | constructor(dataForSEOClient: DataForSEOClient) { 7 | super(dataForSEOClient); 8 | } 9 | 10 | getName(): string { 11 | return 'keywords_data_google_trends_explore'; 12 | } 13 | 14 | getDescription(): string { 15 | return 'This endpoint will provide you with the keyword popularity data from the ‘Explore’ feature of Google Trends. You can check keyword trends for Google Search, Google News, Google Images, Google Shopping, and YouTube'; 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(`keywords 28 | the maximum number of keywords you can specify: 5 29 | the maximum number of characters you can specify in a keyword: 100 30 | the minimum number of characters must be greater than 1 31 | comma characters (,) in the specified keywords will be unset and ignored 32 | Note: keywords cannot consist of a combination of the following characters: < > | \ " - + = ~ ! : * ( ) [ ] { } 33 | 34 | Note: to obtain google_trends_topics_list and google_trends_queries_list items, specify no more than 1 keyword`), 35 | type: z.enum(['web', 'news', 'youtube','images','froogle']).default('web').describe(`google trends type`), 36 | date_from: z.string().optional().describe(`starting date of the time range 37 | if you don’t specify this field, the current day and month of the preceding year will be used by default 38 | minimal value for the web type: 2004-01-01 39 | minimal value for other types: 2008-01-01 40 | date format: "yyyy-mm-dd" 41 | example: 42 | "2019-01-15"`), 43 | date_to: z.string().optional() 44 | .describe( 45 | `ending date of the time range 46 | if you don’t specify this field, the today’s date will be used by default 47 | date format: "yyyy-mm-dd" 48 | example: 49 | "2019-01-15"`), 50 | time_range: z.enum(['past_hour', 'past_4_hours', 'past_day', 'past_7_days', 'past_30_days', 'past_90_days', 'past_12_months', 'past_5_years']) 51 | .default('past_7_days') 52 | .describe( 53 | `preset time ranges 54 | if you specify date_from or date_to parameters, this field will be ignored when setting a task`), 55 | item_types: z.array(z.enum(['google_trends_graph', 'google_trends_map', 'google_trends_topics_list', 'google_trends_queries_list'])) 56 | .default(['google_trends_graph']) 57 | .describe( 58 | `types of items returned 59 | to speed up the execution of the request, specify one item at a time`), 60 | category_code: z.nullable(z.number()).default(null) 61 | .describe( 62 | `google trends search category 63 | you can receive the list of available categories with their category_code by making a separate request to the keywords_data_google_trends_categories tool`) 64 | }; 65 | } 66 | 67 | async handle(params: any): Promise<any> { 68 | try { 69 | const response = await this.dataForSEOClient.makeRequest('/v3/keywords_data/google_trends/explore/live', 'POST', [{ 70 | location_name: params.location_name, 71 | language_code: params.language_code, 72 | keywords: params.keywords, 73 | type: params.type, 74 | date_from: params.date_from, 75 | date_to: params.date_to, 76 | time_range: params.time_range, 77 | item_types: params.item_types, 78 | category_code: params.category_code 79 | }]); 80 | return this.validateAndFormatResponse(response); 81 | } catch (error) { 82 | return this.formatErrorResponse(error); 83 | } 84 | } 85 | } ``` -------------------------------------------------------------------------------- /src/core/modules/onpage/onpage.prompt.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { PromptDefinition } from '../prompt-definition.js'; 3 | 4 | 5 | export const onpagePrompts: PromptDefinition[] = [ 6 | { 7 | name: 'identify_technical_performance_issues_affecting_crawlability_and_ranking', 8 | title: 'Identify technical performance issues affecting crawlability and ranking', 9 | params: { 10 | url: z.string().describe('The URL of the page to analyze'), 11 | }, 12 | handler: async (params) => { 13 | return { 14 | messages: [ 15 | { 16 | role: 'user', 17 | content: { 18 | type: 'text', 19 | text: `Audit page '${params.url}' for crawlability issues, including robots.txt restrictions, noindex tags, and broken internal links. Highlight what's preventing Google from indexing or ranking this page.` 20 | } 21 | } 22 | ] 23 | }; 24 | } 25 | }, 26 | { 27 | name: 'detect_missing_or_duplicate_meta_tags_hurting_seo', 28 | title: 'Detect missing or duplicate meta tags hurting SEO', 29 | params: { 30 | url: z.string().describe('The URL of the page to analyze'), 31 | }, 32 | handler: async (params) => { 33 | return { 34 | messages: [ 35 | { 36 | role: 'user', 37 | content: { 38 | type: 'text', 39 | text: `Review page '${params.url}' to check for missing or duplicate meta title and meta description tags, use validate_micromarkup, enable_javascript, and enable_browser_rendering. Let me know if the tags are too long, too short, or missing, and how to fix them.` 40 | } 41 | } 42 | ] 43 | }; 44 | } 45 | }, 46 | { 47 | name: 'check_for_slow_load_time_and_mobile_compatibility_issues', 48 | title: 'Check for slow load time and mobile compatibility issues', 49 | params: { 50 | url: z.string().describe('The URL of the page to analyze'), 51 | }, 52 | handler: async (params) => { 53 | return { 54 | messages: [ 55 | { 56 | role: 'user', 57 | content: { 58 | type: 'text', 59 | text: `Analyze page '${params.url}' for speed and mobile usability. Use load_resources, enable_javascript, and enable_browser_rendering. Tell me what's slowing it down or making it hard to use on mobile, include the measurements, and give practical steps to improve performance.` 60 | } 61 | } 62 | ] 63 | }; 64 | } 65 | }, 66 | { 67 | name: 'evaluate_internal_linking_and_crawl_depth_for_better_indexing', 68 | title: 'Evaluate internal linking and crawl depth for better indexing', 69 | params: { 70 | url: z.string().describe('The URL of the page to analyze'), 71 | }, 72 | handler: async (params) => { 73 | return { 74 | messages: [ 75 | { 76 | role: 'user', 77 | content: { 78 | type: 'text', 79 | text: `Check how well '${params.url}' is connected internally. Use load_resources, enable_javascript, and enable_browser_rendering. Tell me if it's buried too deep in the site structure or lacks internal links that could help search engines find and rank it. Include the data for each issue or metric.` 80 | } 81 | } 82 | ] 83 | }; 84 | } 85 | }, 86 | { 87 | name: 'analyze_keyword_optimization_and_content_gaps', 88 | title: 'Analyze keyword optimization and content gaps', 89 | params: { 90 | url: z.string().describe('The URL of the page to analyze'), 91 | keyword: z.string().describe('The primary keyword to optimize for'), 92 | }, 93 | handler: async (params) => { 94 | return { 95 | messages: [ 96 | { 97 | role: 'user', 98 | content: { 99 | type: 'text', 100 | text: `Evaluate '${params.url}' for how well it's optimized for the keyword '${params.keyword}'. Analyze on-page SEO elements like title, meta description, headings (H1-H6), internal links, and keyword usage, extract and parse all content elements (headings, paragraphs, alt attributes, etc.), and check for keyword placement and semantic relevance. Identify missing keyword placements and content gaps that could affect its relevance and ranking.` 101 | } 102 | } 103 | ] 104 | }; 105 | } 106 | }, 107 | ] ``` -------------------------------------------------------------------------------- /src/core/modules/dataforseo-labs/tools/google/competitor-research/google-serp-competitors.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 GoogleSERPCompetitorsTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'dataforseo_labs_google_serp_competitors'; 12 | } 13 | 14 | getDescription(): string { 15 | return "This endpoint will provide you with a list of domains ranking for the keywords you specify. You will also get SERP rankings, rating, estimated traffic volume, and visibility values the provided domains gain from the specified keywords."; 16 | } 17 | 18 | getParams(): z.ZodRawShape { 19 | return { 20 | keywords: z.array(z.string()).describe(`keywords array 21 | required field 22 | the results will be based on the keywords you specify in this array 23 | UTF-8 encoding; 24 | the keywords will be converted to lowercase format; 25 | you can specify the maximum of 200 keywords`), 26 | location_name: z.string().default("United States").describe(`full name of the location 27 | required field 28 | only in format "Country" (not "City" or "Region") 29 | example: 30 | 'United Kingdom', 'United States', 'Canada'`), 31 | language_code: z.string().default("en").describe( 32 | `language code 33 | required field 34 | example: 35 | en`), 36 | limit: z.number().min(1).max(1000).default(10).optional().describe("Maximum number of keywords to return"), 37 | offset: z.number().min(0).optional().describe( 38 | `offset in the results array of returned keywords 39 | optional field 40 | default value: 0 41 | if you specify the 10 value, the first ten keywords in the results array will be omitted and the data will be provided for the successive keywords` 42 | ), 43 | filters: this.getFilterExpression().optional().describe( 44 | `you can add several filters at once (8 filters maximum) 45 | you should set a logical operator and, or between the conditions 46 | the following operators are supported: 47 | regex, not_regex, <, <=, >, >=, =, <>, in, not_in, match, not_match, ilike, not_ilike, like, not_like 48 | you can use the % operator with like and not_like, as well as ilike and not_ilike to match any string of zero or more characters 49 | merge operator must be a string and connect two other arrays, availible values: or, and. 50 | example: 51 | ["ranked_serp_element.serp_item.rank_group","<=",10] 52 | [["ranked_serp_element.serp_item.rank_group","<=",10],"or",["ranked_serp_element.serp_item.type","<>","paid"]] 53 | [["keyword_data.keyword_info.search_volume","<>",0],"and",[["ranked_serp_element.serp_item.type","<>","paid"],"or",["ranked_serp_element.serp_item.is_malicious","=",false]]]` 54 | ), 55 | order_by: z.array(z.string()).optional().describe( 56 | `results sorting rules 57 | optional field 58 | you can use the same values as in the filters array to sort the results 59 | possible sorting types: 60 | asc – results will be sorted in the ascending order 61 | desc – results will be sorted in the descending order 62 | the comma is used as a separator 63 | example: 64 | ["avg_position,asc"] 65 | default rule: 66 | ["rating,desc"] 67 | note that you can set no more than three sorting rules in a single request 68 | you should use a comma to separate several sorting rules 69 | example: 70 | ["avg_position,asc","etv,desc"]` 71 | ), 72 | include_subdomains: z.boolean().optional().describe("Include keywords from subdomains") 73 | }; 74 | } 75 | 76 | async handle(params: any): Promise<any> { 77 | try { 78 | const response = await this.client.makeRequest('/v3/dataforseo_labs/google/serp_competitors/live', 'POST', [{ 79 | keywords: params.keywords, 80 | location_name: params.location_name, 81 | language_code: params.language_code, 82 | limit: params.limit, 83 | offset: params.offset, 84 | filters: this.formatFilters(params.filters), 85 | order_by: this.formatOrderBy(params.order_by), 86 | }]); 87 | return this.validateAndFormatResponse(response); 88 | } catch (error) { 89 | return this.formatErrorResponse(error); 90 | } 91 | } 92 | } ``` -------------------------------------------------------------------------------- /src/core/modules/backlinks/backlinks-api.module.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { DataForSEOClient } from '../../client/dataforseo.client.js'; 2 | import { BaseModule, ToolDefinition } from '../base.module.js'; 3 | import { PromptDefinition } from '../prompt-definition.js'; 4 | import { backlinksPrompts } from './backlinks.prompt.js'; 5 | import { BacklinksAnchorTool } from './tools/backlinks-anchor.tool.js'; 6 | import { BacklinksTool } from './tools/backlinks-backlinks.tool.js'; 7 | import { BacklinksBulkBacklinksTool } from './tools/backlinks-bulk-backlinks.tool.js'; 8 | import { BacklinksBulkNewLostBacklinksTool } from './tools/backlinks-bulk-new-lost-backlinks.tool.js'; 9 | import { BacklinksBulkNewLostReferringDomainsTool } from './tools/backlinks-bulk-new-lost-referring-domains.tool.js'; 10 | import { BacklinksBulkPagesSummaryTool } from './tools/backlinks-bulk-pages-summary.js'; 11 | import { BacklinksBulkRanksTool } from './tools/backlinks-bulk-ranks.tool.js'; 12 | import { BacklinksBulkReferringDomainsTool } from './tools/backlinks-bulk-referring-domains.tool.js'; 13 | import { BacklinksBulkSpamScoreTool } from './tools/backlinks-bulk-spam-score.tool.js'; 14 | import { BacklinksCompetitorsTool } from './tools/backlinks-competitors.tool.js'; 15 | import { BacklinksDomainIntersectionTool } from './tools/backlinks-domain-intersection.tool.js'; 16 | import { BacklinksDomainPagesSummaryTool } from './tools/backlinks-domain-pages-summary.tool.js'; 17 | import { BacklinksDomainPagesTool } from './tools/backlinks-domain-pages.tool.js'; 18 | import { BacklinksFiltersTool } from './tools/backlinks-filters.tool.js'; 19 | import { BacklinksPageIntersectionTool } from './tools/backlinks-page-intersection.tool.js'; 20 | import { BacklinksReferringDomainsTool } from './tools/backlinks-referring-domains.tool.js'; 21 | import { BacklinksReferringNetworksTool } from './tools/backlinks-referring-networks.tool.js'; 22 | import { BacklinksSummaryTool } from './tools/backlinks-summary.tool.js'; 23 | import { BacklinksTimeseriesNewLostSummaryTool } from './tools/backlinks-timeseries-new-lost-summary.tool.js'; 24 | import { BacklinksTimeseriesSummaryTool } from './tools/backlinks-timeseries-summary.tool.js'; 25 | 26 | export class BacklinksApiModule extends BaseModule { 27 | constructor(client: DataForSEOClient) { 28 | super(client); 29 | } 30 | 31 | getTools(): Record<string, ToolDefinition> { 32 | const tools = [ 33 | new BacklinksTool(this.dataForSEOClient), 34 | new BacklinksAnchorTool(this.dataForSEOClient), 35 | new BacklinksBulkBacklinksTool(this.dataForSEOClient), 36 | new BacklinksBulkNewLostReferringDomainsTool(this.dataForSEOClient), 37 | new BacklinksBulkNewLostBacklinksTool(this.dataForSEOClient), 38 | new BacklinksBulkRanksTool(this.dataForSEOClient), 39 | new BacklinksBulkReferringDomainsTool(this.dataForSEOClient), 40 | new BacklinksBulkSpamScoreTool(this.dataForSEOClient), 41 | new BacklinksCompetitorsTool(this.dataForSEOClient), 42 | new BacklinksDomainIntersectionTool(this.dataForSEOClient), 43 | new BacklinksDomainPagesSummaryTool(this.dataForSEOClient), 44 | new BacklinksDomainPagesTool(this.dataForSEOClient), 45 | new BacklinksPageIntersectionTool(this.dataForSEOClient), 46 | new BacklinksReferringDomainsTool(this.dataForSEOClient), 47 | new BacklinksReferringNetworksTool(this.dataForSEOClient), 48 | new BacklinksSummaryTool(this.dataForSEOClient), 49 | new BacklinksTimeseriesNewLostSummaryTool(this.dataForSEOClient), 50 | new BacklinksTimeseriesSummaryTool(this.dataForSEOClient), 51 | new BacklinksBulkPagesSummaryTool(this.dataForSEOClient), 52 | new BacklinksFiltersTool(this.dataForSEOClient) 53 | // Add more tools here 54 | ]; 55 | 56 | return tools.reduce((acc, tool) => ({ 57 | ...acc, 58 | [tool.getName()]: { 59 | description: tool.getDescription(), 60 | params: tool.getParams(), 61 | handler: (params: any) => tool.handle(params), 62 | }, 63 | }), {}); 64 | } 65 | 66 | getPrompts(): Record<string, PromptDefinition> { 67 | return backlinksPrompts.reduce((acc, prompt) => ({ 68 | ...acc, 69 | [prompt.name]: { 70 | description: prompt.description, 71 | params: prompt.params, 72 | handler: (params: any) => { 73 | 74 | return prompt.handler(params); 75 | }, 76 | }, 77 | }), {}); 78 | } 79 | } ``` -------------------------------------------------------------------------------- /src/core/modules/business-data-api/tools/listings/business-listings-search.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 BusinessDataBusinessListingsSearchTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'business_data_business_listings_search'; 12 | } 13 | 14 | getDescription(): string { 15 | return `Business Listings Search API provides results containing information about business entities listed on Google Maps in the specified categories. You will receive the address, contacts, rating, working hours, and other relevant data`; 16 | } 17 | 18 | getParams(): z.ZodRawShape { 19 | return { 20 | description: z.string().optional().describe(`description of the element in SERP 21 | optional field 22 | the description of the business entity for which the results are collected; 23 | can contain up to 200 characters`), 24 | title: z.string().optional().describe(`title of the element in SERP 25 | optional field 26 | the name of the business entity for which the results are collected; 27 | can contain up to 200 characters`), 28 | categories: z.array(z.string()).optional().describe(`business categories 29 | the categories you specify are used to search for business listings; 30 | if you don’t use this field, we will return business listings found in the specified location; 31 | you can specify up to 10 categories`), 32 | location_coordinate: z.string().optional().describe(`GPS coordinates of a location 33 | optional field 34 | location_coordinate parameter should be specified in the “latitude,longitude,radius” format 35 | the maximum number of decimal digits for “latitude” and “longitude”: 7 36 | the value of “radius” is specified in kilometres (km) 37 | the minimum value for “radius”: 1 38 | the maximum value for “radius”: 100000 39 | example: 40 | 53.476225,-2.243572,200`), 41 | limit: z.number().min(1).max(1000).default(10).optional().describe("the maximum number of returned businesses"), 42 | offset: z.number().min(0).optional().describe( 43 | `offset in the results array of returned businesses 44 | optional field 45 | default value: 0 46 | 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` 47 | ), 48 | filters: this.getFilterExpression().optional().describe( 49 | `array of results filtering parameters 50 | optional field 51 | you can add several filters at once (8 filters maximum) 52 | you should set a logical operator and, or between the conditions 53 | the following operators are supported: 54 | regex, not_regex, <, <=, >, >=, =, <>, in, not_in, like, not_like, match, not_match 55 | you can use the % operator with like and not_like to match any string of zero or more characters 56 | example: 57 | ["rating.value",">",3]` 58 | ), 59 | order_by: z.array(z.string()).optional().describe( 60 | `results sorting rules 61 | optional field 62 | you can use the same values as in the filters array to sort the results 63 | possible sorting types: 64 | asc – results will be sorted in the ascending order 65 | desc – results will be sorted in the descending order 66 | you should use a comma to set up a sorting parameter 67 | example: 68 | ["rating.value,desc"]note that you can set no more than three sorting rules in a single request 69 | you should use a comma to separate several sorting rules 70 | example: 71 | ["rating.value,desc","rating.votes_count,desc"]` 72 | ), 73 | is_claimed: z.boolean().optional().describe(`indicates whether the business is verified by its owner on Google Maps`).default(true) 74 | }; 75 | } 76 | 77 | async handle(params: any): Promise<any> { 78 | try { 79 | const response = await this.client.makeRequest('/v3/business_data/business_listings/search/live', 'POST', [{ 80 | description: params.description, 81 | title: params.title, 82 | categories: params.categories, 83 | limit: params.limit, 84 | offset: params.offset, 85 | filters: this.formatFilters(params.filters), 86 | order_by: this.formatOrderBy(params.order_by), 87 | location_coordinate: params.location_coordinate 88 | }]); 89 | return this.validateAndFormatResponse(response); 90 | } catch (error) { 91 | return this.formatErrorResponse(error); 92 | } 93 | } 94 | } ``` -------------------------------------------------------------------------------- /src/core/modules/dataforseo-labs/tools/google/competitor-research/google-domain-competitors.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 GoogleDomainCompetitorsTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'dataforseo_labs_google_competitors_domain'; 12 | } 13 | 14 | getDescription(): string { 15 | return `This endpoint will provide you with a full overview of ranking and traffic data of the competitor domains from organic and paid search. In addition to that, you will get the metrics specific to the keywords both competitor domains and your domain rank for within the same SERP.`; 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 | limit: z.number().min(1).max(1000).default(10).optional().describe("Maximum number of keywords to return"), 34 | offset: z.number().min(0).optional().describe( 35 | `offset in the results array of returned keywords 36 | optional field 37 | default value: 0 38 | if you specify the 10 value, the first ten keywords in the results array will be omitted and the data will be provided for the successive keywords` 39 | ), 40 | filters: this.getFilterExpression().optional().describe( 41 | `you can add several filters at once (8 filters maximum) 42 | you should set a logical operator and, or between the conditions 43 | the following operators are supported: 44 | regex, not_regex, <, <=, >, >=, =, <>, in, not_in, match, not_match, ilike, not_ilike, like, not_like 45 | you can use the % operator with like and not_like, as well as ilike and not_ilike to match any string of zero or more characters 46 | merge operator must be a string and connect two other arrays, availible values: or, and. 47 | example: 48 | ["metrics.organic.count",">",50] 49 | [["metrics.organic.pos_1","<>",0],"and",["metrics.organic.impressions_etv",">=","10"]] 50 | 51 | [[["metrics.organic.count",">=",50],"and",["metrics.organic.pos_1","in",[1,5]]], 52 | "or", 53 | ["metrics.organic.etv",">=","100"]]` 54 | ), 55 | order_by: z.array(z.string()).optional().describe( 56 | `results sorting rules 57 | optional field 58 | you can use the same values as in the filters array to sort the results 59 | possible sorting types: 60 | asc – results will be sorted in the ascending order 61 | desc – results will be sorted in the descending order 62 | you should use a comma to set up a sorting parameter 63 | default rule: 64 | ["relevance,desc"] 65 | example: 66 | ["relevance,desc","keyword_info.search_volume,desc"]` 67 | ), 68 | exclude_top_domains: z.boolean().default(true).describe(`indicates whether to exclude world's largest websites 69 | optional field 70 | default value: false 71 | set to true if you want to get highly-relevant competitors excluding the top websites`), 72 | include_clickstream_data: z.boolean().optional().default(false).describe( 73 | `Include or exclude data from clickstream-based metrics in the result`) 74 | 75 | }; 76 | } 77 | 78 | async handle(params: any): Promise<any> { 79 | try { 80 | const response = await this.client.makeRequest('/v3/dataforseo_labs/google/competitors_domain/live', 'POST', [{ 81 | target: params.target, 82 | location_name: params.location_name, 83 | language_code: params.language_code, 84 | ignore_synonyms: params.ignore_synonyms, 85 | filters: this.formatFilters(params.filters), 86 | order_by: this.formatOrderBy(params.order_by), 87 | exclude_top_domains: params.exclude_top_domains, 88 | item_types: ['organic'], 89 | include_clickstream_data: params.include_clickstream_data 90 | }]); 91 | return this.validateAndFormatResponse(response); 92 | } catch (error) { 93 | return this.formatErrorResponse(error); 94 | } 95 | } 96 | } ``` -------------------------------------------------------------------------------- /src/core/modules/content-analysis/tools/content-analysis-summary.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { any, z } from 'zod'; 2 | import { BaseTool } from '../../base.tool.js'; 3 | import { DataForSEOClient } from '../../../client/dataforseo.client.js'; 4 | 5 | export class ContentAnalysisSummaryTool extends BaseTool { 6 | constructor(dataForSEOClient: DataForSEOClient) { 7 | super(dataForSEOClient); 8 | } 9 | 10 | getName(): string { 11 | return 'content_analysis_summary'; 12 | } 13 | 14 | getDescription(): string { 15 | return `This endpoint will provide you with an overview of citation data available for the target keyword`; 16 | } 17 | 18 | getParams(): z.ZodRawShape { 19 | return { 20 | keyword: z.string().describe(`target keyword 21 | Note: to match an exact phrase instead of a stand-alone keyword, use double quotes and backslashes;`), 22 | keyword_fields: z.object({ 23 | title: z.string().optional(), 24 | main_title: z.string().optional(), 25 | previous_title: z.string().optional(), 26 | snippet: z.string().optional() 27 | }).optional().describe( 28 | `target keyword fields and target keywords 29 | use this parameter to filter the dataset by keywords that certain fields should contain; 30 | you can indicate several fields; 31 | Note: to match an exact phrase instead of a stand-alone keyword, use double quotes and backslashes; 32 | example: 33 | { 34 | "snippet": "\\"logitech mouse\\"", 35 | "main_title": "sale" 36 | }` 37 | ), 38 | page_type: z.array(z.enum(['ecommerce','news','blogs', 'message-boards','organization'])).optional().describe(`target page types`), 39 | initial_dataset_filters: this.getFilterExpression().optional().describe( 40 | `initial dataset filtering parameters 41 | initial filtering parameters that apply to fields in the Search endpoint; 42 | you can add several filters at once (8 filters maximum); 43 | you should set a logical operator and, or between the conditions; 44 | the following operators are supported: 45 | regex, not_regex, <, <=, >, >=, =, <>, in, not_in, like,not_like, has, has_not, match, not_match 46 | you can use the % operator with like and not_like to match any string of zero or more characters; 47 | example: 48 | ["domain","<>", "logitech.com"] 49 | [["domain","<>","logitech.com"],"and",["content_info.connotation_types.negative",">",1000]] 50 | 51 | [["domain","<>","logitech.com"]], 52 | "and", 53 | [["content_info.connotation_types.negative",">",1000], 54 | "or", 55 | ["content_info.text_category","has",10994]]` 56 | ), 57 | positive_connotation_threshold: z.number() 58 | .describe(`positive connotation threshold 59 | specified as the probability index threshold for positive sentiment related to the citation content 60 | if you specify this field, connotation_types object in the response will only contain data on citations with positive sentiment probability more than or equal to the specified value`).min(0).max(1).optional().default(0.4), 61 | sentiments_connotation_threshold: z.number() 62 | .describe(`sentiment connotation threshold 63 | specified as the probability index threshold for sentiment connotations related to the citation content 64 | if you specify this field, sentiment_connotations object in the response will only contain data on citations where the 65 | probability per each sentiment is more than or equal to the specified value`) 66 | .min(0).max(1).optional().default(0.4), 67 | internal_list_limit: z.number().min(1).max(20).default(1) 68 | .describe( 69 | `maximum number of elements within internal arrays 70 | you can use this field to limit the number of elements within the following arrays`) 71 | .optional(), 72 | 73 | }; 74 | } 75 | 76 | async handle(params: any): Promise<any> { 77 | try { 78 | const response = await this.dataForSEOClient.makeRequest('/v3/content_analysis/summary/live', 'POST', [{ 79 | keyword: params.keyword, 80 | keyword_fields: params.keyword_fields, 81 | page_type: params.page_type, 82 | initial_dataset_filters: this.formatFilters(params.initial_dataset_filters), 83 | positive_connotation_threshold: params.positive_connotation_threshold, 84 | sentiments_connotation_threshold: params.sentiments_connotation_threshold, 85 | internal_list_limit: params.internal_list_limit 86 | }]); 87 | return this.validateAndFormatResponse(response); 88 | } catch (error) { 89 | return this.formatErrorResponse(error); 90 | } 91 | } 92 | } ``` -------------------------------------------------------------------------------- /src/core/modules/dataforseo-labs/tools/google/competitor-research/google-subdomains.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 GoogleSubdomainsTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'dataforseo_labs_google_subdomains'; 12 | } 13 | 14 | getDescription(): string { 15 | return `This endpoint will provide you with a list of subdomains of the specified domain, along with the ranking distribution across organic and paid search. In addition to that, you will also get the estimated traffic volume of subdomains based on search volume.`; 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 | limit: z.number().min(1).max(1000).default(10).optional().describe("Maximum number of keywords to return"), 34 | offset: z.number().min(0).optional().describe( 35 | `offset in the results array of returned keywords 36 | optional field 37 | default value: 0 38 | if you specify the 10 value, the first ten keywords in the results array will be omitted and the data will be provided for the successive keywords` 39 | ), 40 | filters: this.getFilterExpression().optional().describe( 41 | `you can add several filters at once (8 filters maximum) 42 | you should set a logical operator and, or between the conditions 43 | the following operators are supported: 44 | regex, not_regex, <, <=, >, >=, =, <>, in, not_in, match, not_match, ilike, not_ilike, like, not_like 45 | you can use the % operator with like and not_like, as well as ilike and not_ilike to match any string of zero or more characters 46 | merge operator must be a string and connect two other arrays, availible values: or, and. 47 | example: 48 | ["metrics.organic.count",">",50] 49 | [["metrics.organic.pos_1","<>",0],"and",["metrics.organic.impressions_etv",">=","10"]] 50 | [[["metrics.organic.count",">=",50],"and",["metrics.organic.pos_1","in",[1,5]]],"or",["metrics.organic.etv",">=","100"]]` 51 | ), 52 | order_by: z.array(z.string()).optional().describe( 53 | `results sorting rules 54 | optional field 55 | you can use the same values as in the filters array to sort the results 56 | possible sorting types: 57 | asc – results will be sorted in the ascending order 58 | desc – results will be sorted in the descending order 59 | you should use a comma to specify a sorting type 60 | example: 61 | ["metrics.paid.etv,asc"] 62 | Note: you can set no more than three sorting rules in a single request 63 | you should use a comma to separate several sorting rules 64 | example: 65 | ["metrics.organic.etv,desc","metrics.paid.count,asc"] 66 | default rule: 67 | ["metrics.organic.count,desc"]` 68 | ), 69 | item_types: z.array(z.string()).optional().describe( 70 | `item types to return 71 | optional field 72 | default: ['organic'] 73 | possible values: 74 | organic 75 | paid` 76 | ), 77 | include_clickstream_data: z.boolean().optional().default(false).describe( 78 | `Include or exclude data from clickstream-based metrics in the result`) 79 | 80 | }; 81 | } 82 | 83 | async handle(params: any): Promise<any> { 84 | try { 85 | const response = await this.client.makeRequest('/v3/dataforseo_labs/google/subdomains/live', 'POST', [{ 86 | target: params.target, 87 | location_name: params.location_name, 88 | language_code: params.language_code, 89 | ignore_synonyms: params.ignore_synonyms, 90 | filters: this.formatFilters(params.filters), 91 | order_by: this.formatOrderBy(params.order_by), 92 | exclude_top_domains: params.exclude_top_domains, 93 | item_types: params.item_types, 94 | include_clickstream_data: params.include_clickstream_data, 95 | limit: params.limit, 96 | offset: params.offset 97 | }]); 98 | return this.validateAndFormatResponse(response); 99 | } catch (error) { 100 | return this.formatErrorResponse(error); 101 | } 102 | } 103 | } ``` -------------------------------------------------------------------------------- /src/core/modules/dataforseo-labs/tools/google/competitor-research/google-ranked-keywords.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 GoogleRankedKeywordsTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'dataforseo_labs_google_ranked_keywords'; 12 | } 13 | 14 | getDescription(): string { 15 | return "This endpoint will provide you with the list of keywords that any domain or webpage is ranking for. You will also get SERP elements related to the keyword position, as well as impressions, monthly searches and other data relevant to the returned keywords."; 16 | } 17 | 18 | getParams(): z.ZodRawShape { 19 | return { 20 | target: z.string().describe(`domain name or page url 21 | required field 22 | the domain name of the target website or URL of the target webpage; 23 | the domain name must be specified without https:// or www.; 24 | the webpage URL must be specified with https:// or www. 25 | Note: if you specify the webpage URL without https:// or www., the result will be returned for the entire domain rather than the specific page 26 | `), 27 | location_name: z.string().default("United States").describe(`full name of the location 28 | required field 29 | only in format "Country" (not "City" or "Region") 30 | example: 31 | 'United Kingdom', 'United States', 'Canada'`), 32 | language_code: z.string().default("en").describe( 33 | `language code 34 | required field 35 | example: 36 | en`), 37 | limit: z.number().min(1).max(1000).default(10).optional().describe("Maximum number of keywords to return"), 38 | offset: z.number().min(0).optional().describe( 39 | `offset in the results array of returned keywords 40 | optional field 41 | default value: 0 42 | if you specify the 10 value, the first ten keywords in the results array will be omitted and the data will be provided for the successive keywords` 43 | ), 44 | filters: this.getFilterExpression().optional().describe( 45 | `Array of filter conditions and logical operators. Each filter condition is an array of [field, operator, value]. 46 | Maximum 8 filters allowed. 47 | Available operators: =, <>, <, <=, >, >=, in, not_in, like, not_like, ilike, not_ilike, regex, not_regex, match, not_match 48 | Logical operators: "and", "or" 49 | Examples: 50 | Simple filter: [["ranked_serp_element.serp_item.rank_group","<=",10]] 51 | With logical operator: [["ranked_serp_element.serp_item.rank_group","<=",10],"or",["ranked_serp_element.serp_item.type","<>","paid"]] 52 | Complex filter: [["keyword_data.keyword_info.search_volume","<>",0],"and",[["ranked_serp_element.serp_item.type","<>","paid"],"or",["ranked_serp_element.serp_item.is_malicious","=",false]]]` 53 | ), 54 | order_by: z.array(z.string()).optional().describe( 55 | `results sorting rules 56 | optional field 57 | you can use the same values as in the filters array to sort the results 58 | possible sorting types: 59 | asc – results will be sorted in the ascending order 60 | desc – results will be sorted in the descending order 61 | you should use a comma to set up a sorting type 62 | example: 63 | ["keyword_data.keyword_info.competition,desc"] 64 | default rule: 65 | ["ranked_serp_element.serp_item.rank_group,asc"] 66 | note that you can set no more than three sorting rules in a single request 67 | you should use a comma to separate several sorting rules 68 | example: 69 | ["keyword_data.keyword_info.search_volume,desc","keyword_data.keyword_info.cpc,desc"]` 70 | ), 71 | include_subdomains: z.boolean().optional().describe("Include keywords from subdomains"), 72 | include_clickstream_data: z.boolean().optional().default(false).describe( 73 | `Include or exclude data from clickstream-based metrics in the result`) 74 | }; 75 | } 76 | 77 | async handle(params: any): Promise<any> { 78 | try { 79 | const response = await this.client.makeRequest('/v3/dataforseo_labs/google/ranked_keywords/live', 'POST', [{ 80 | target: params.target, 81 | location_name: params.location_name, 82 | language_code: params.language_code, 83 | limit: params.limit, 84 | offset: params.offset, 85 | filters: this.formatFilters(params.filters), 86 | order_by: this.formatOrderBy(params.order_by), 87 | include_subdomains: params.include_subdomains, 88 | include_clickstream_data: params.include_clickstream_data 89 | }]); 90 | return this.validateAndFormatResponse(response); 91 | } catch (error) { 92 | return this.formatErrorResponse(error); 93 | } 94 | } 95 | } ``` -------------------------------------------------------------------------------- /src/core/modules/dataforseo-labs/tools/google/keyword-research/google-keywords-suggestions.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 GoogleKeywordsSuggestionsTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'dataforseo_labs_google_keyword_suggestions'; 12 | } 13 | 14 | getDescription(): string { 15 | return `The Keyword Suggestions provides search queries that include the specified seed keyword. 16 | 17 | The algorithm is based on the full-text search for the specified keyword and therefore returns only those search terms that contain the keyword you set in the POST array with additional words before, after, or within the specified key phrase. Returned keyword suggestions can contain the words from the specified key phrase in a sequence different from the one you specify. 18 | 19 | As a result, you will get a list of long-tail keywords with each keyword in the list matching the specified search term. 20 | 21 | Along with each suggested keyword, you will get its search volume rate for the last month, search volume trend for the previous 12 months, as well as current cost-per-click and competition values. Moreover, this endpoint supplies minimum, maximum and average values of daily impressions, clicks and CPC for each result. 22 | 23 | `; 24 | } 25 | 26 | getParams(): z.ZodRawShape { 27 | return { 28 | keyword: z.string().describe(`target keyword`), 29 | location_name: z.string().default("United States").describe(`full name of the location 30 | required field 31 | only in format "Country" (not "City" or "Region") 32 | example: 33 | 'United Kingdom', 'United States', 'Canada'`), 34 | language_code: z.string().default("en").describe( 35 | `language code 36 | required field 37 | example: 38 | en`), 39 | limit: z.number().min(1).max(1000).default(10).optional().describe("Maximum number of keywords to return"), 40 | offset: z.number().min(0).optional().describe( 41 | `offset in the results array of returned keywords 42 | optional field 43 | default value: 0 44 | if you specify the 10 value, the first ten keywords in the results array will be omitted and the data will be provided for the successive keywords` 45 | ), 46 | filters: this.getFilterExpression().optional().describe( 47 | `you can add several filters at once (8 filters maximum) 48 | you should set a logical operator and, or between the conditions 49 | the following operators are supported: 50 | regex, not_regex, <, <=, >, >=, =, <>, in, not_in, match, not_match, ilike, not_ilike, like, not_like 51 | you can use the % operator with like and not_like, as well as ilike and not_ilike to match any string of zero or more characters 52 | merge operator must be a string and connect two other arrays, availible values: or, and. 53 | example: 54 | ["keyword_info.search_volume",">",0] 55 | [["keyword_info.search_volume","in",[0,1000]], 56 | "and", 57 | ["keyword_info.competition_level","=","LOW"]][["keyword_info.search_volume",">",100], 58 | "and", 59 | [["keyword_info.cpc","<",0.5], 60 | "or", 61 | ["keyword_info.high_top_of_page_bid","<=",0.5]]]` 62 | ), 63 | order_by: z.array(z.string()).optional().describe( 64 | `results sorting rules 65 | optional field 66 | you can use the same values as in the filters array to sort the results 67 | possible sorting types: 68 | asc – results will be sorted in the ascending order 69 | desc – results will be sorted in the descending order 70 | a comma is used as a separator 71 | example: 72 | ["keyword_info.competition,desc"] 73 | default rule: 74 | ["keyword_info.search_volume,desc"] 75 | note that you can set no more than three sorting rules in a single request 76 | you should use a comma to separate several sorting rules 77 | example: 78 | ["keyword_info.search_volume,desc","keyword_info.cpc,desc"]` 79 | ), 80 | include_clickstream_data: z.boolean().optional().default(false).describe( 81 | `Include or exclude data from clickstream-based metrics in the result`) 82 | }; 83 | } 84 | 85 | async handle(params: any): Promise<any> { 86 | try { 87 | const response = await this.client.makeRequest('/v3/dataforseo_labs/google/keyword_suggestions/live', 'POST', [{ 88 | keyword: params.keyword, 89 | location_name: params.location_name, 90 | language_code: params.language_code, 91 | limit: params.limit, 92 | offset: params.offset, 93 | filters: this.formatFilters(params.filters), 94 | order_by: this.formatOrderBy(params.order_by), 95 | include_clickstream_data: params.include_clickstream_data 96 | }]); 97 | return this.validateAndFormatResponse(response); 98 | } catch (error) { 99 | return this.formatErrorResponse(error); 100 | } 101 | } 102 | } ``` -------------------------------------------------------------------------------- /src/core/modules/dataforseo-labs/tools/google/keyword-research/google-related-keywords.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 GoogleRelatedKeywordsTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'dataforseo_labs_google_related_keywords'; 12 | } 13 | 14 | getDescription(): string { 15 | return `The Related Keywords endpoint provides keywords appearing in the 16 | "searches related to" SERP element 17 | You can get up to 4680 keyword ideas by specifying the search depth. Each related keyword comes with the list of relevant product categories, search volume rate for the last month, search volume trend for the previous 12 months, as well as current cost-per-click and competition values. Moreover, this endpoint supplies minimum, maximum and average values of daily impressions, clicks and CPC for each result. 18 | 19 | Datasource: DataForSEO SERPs Database 20 | Search algorithm: depth-first search for queries appearing in the "search related to" element of SERP for the specified seed keyword. 21 | `; 22 | } 23 | 24 | getParams(): z.ZodRawShape { 25 | return { 26 | keyword: z.string().describe(`target keyword`), 27 | depth: z.number().min(0).max(4).default(1).describe(`keyword search depth`), 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 | limit: z.number().min(1).max(1000).default(10).optional().describe("Maximum number of keywords to return"), 39 | offset: z.number().min(0).optional().describe( 40 | `offset in the results array of returned keywords 41 | optional field 42 | default value: 0 43 | if you specify the 10 value, the first ten keywords in the results array will be omitted and the data will be provided for the successive keywords` 44 | ), 45 | filters: this.getFilterExpression().optional().describe( 46 | `you can add several filters at once (8 filters maximum) 47 | you should set a logical operator and, or between the conditions 48 | the following operators are supported: 49 | regex, not_regex, <, <=, >, >=, =, <>, in, not_in, match, not_match, ilike, not_ilike, like, not_like 50 | you can use the % operator with like and not_like, as well as ilike and not_ilike to match any string of zero or more characters 51 | merge operator must be a string and connect two other arrays, availible values: or, and. 52 | example: 53 | ["keyword_data.keyword_info.search_volume",">",0] 54 | [["keyword_info.search_volume","in",[0,1000]], 55 | "and", 56 | ["keyword_data.keyword_info.competition_level","=","LOW"]] 57 | 58 | [["keyword_data.keyword_info.search_volume",">",100], 59 | "and", 60 | [["keyword_data.keyword_info.cpc","<",0.5], 61 | "or", 62 | ["keyword_data.keyword_info.high_top_of_page_bid","<=",0.5]]]` 63 | ), 64 | order_by: z.array(z.string()).optional().describe( 65 | `results sorting rules 66 | optional field 67 | you can use the same values as in the filters array to sort the results 68 | possible sorting types: 69 | asc – results will be sorted in the ascending order 70 | desc – results will be sorted in the descending order 71 | you should use a comma to set up a sorting type 72 | example: 73 | ["keyword_data.keyword_info.competition,desc"] 74 | default rule: 75 | ["keyword_data.keyword_info.search_volume,desc"] 76 | note that you can set no more than three sorting rules in a single request 77 | you should use a comma to separate several sorting rules 78 | example: 79 | ["keyword_data.keyword_info.search_volume,desc","keyword_data.keyword_info.cpc,desc"]` 80 | ), 81 | include_clickstream_data: z.boolean().optional().default(false).describe( 82 | `Include or exclude data from clickstream-based metrics in the result`) 83 | }; 84 | } 85 | 86 | async handle(params: any): Promise<any> { 87 | try { 88 | const response = await this.client.makeRequest('/v3/dataforseo_labs/google/related_keywords/live', 'POST', [{ 89 | keyword: params.keyword, 90 | location_name: params.location_name, 91 | language_code: params.language_code, 92 | depth: params.depth, 93 | limit: params.limit, 94 | offset: params.offset, 95 | filters: this.formatFilters(params.filters), 96 | order_by: this.formatOrderBy(params.order_by), 97 | include_clickstream_data: params.include_clickstream_data 98 | }]); 99 | return this.validateAndFormatResponse(response); 100 | } catch (error) { 101 | return this.formatErrorResponse(error); 102 | } 103 | } 104 | } ``` -------------------------------------------------------------------------------- /src/core/modules/dataforseo-labs/dataforseo-labs-api.module.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { DataForSEOClient } from '../../client/dataforseo.client.js'; 2 | import { BaseModule, ToolDefinition } from '../base.module.js'; 3 | import { PromptDefinition } from '../prompt-definition.js'; 4 | import { GoogleDomainCompetitorsTool } from './tools/google/competitor-research/google-domain-competitors.tool.js'; 5 | import { GoogleDomainRankOverviewTool } from './tools/google/competitor-research/google-domain-rank-overview.tool.js'; 6 | import { GoogleKeywordsIdeasTool } from './tools/google/keyword-research/google-keywords-ideas.tool.js'; 7 | import { GoogleKeywordsSuggestionsTool } from './tools/google/keyword-research/google-keywords-suggestions.tool.js'; 8 | import { GoogleRankedKeywordsTool } from './tools/google/competitor-research/google-ranked-keywords.tool.js'; 9 | import { GoogleRelatedKeywordsTool } from './tools/google/keyword-research/google-related-keywords.tool.js'; 10 | import { GoogleBulkKeywordDifficultyTool } from './tools/google/keyword-research/google-bulk-keyword-difficulty.tool.js'; 11 | import { GoogleTopSearchesTool } from './tools/google/market-analysis/google-top-searches.tool.js'; 12 | import { GoogleKeywordOverviewTool } from './tools/google/keyword-research/google-keyword-overview.tool.js'; 13 | import { GoogleKeywordsForSiteTool } from './tools/google/keyword-research/google-keywords-for-site.tool.js'; 14 | import { GoogleSubdomainsTool } from './tools/google/competitor-research/google-subdomains.js'; 15 | import { GoogleSERPCompetitorsTool } from './tools/google/competitor-research/google-serp-competitors.tool.js'; 16 | import { GoogleHistoricalSERP } from './tools/google/competitor-research/google-historical-serp.js'; 17 | import { GoogleSearchIntentTool } from './tools/google/keyword-research/google-search-intent.tool.js'; 18 | import { GoogleDomainIntersectionsTool } from './tools/google/competitor-research/google-domain-intersection.tool.js'; 19 | import { GoogleHistoricalDomainRankOverviewTool } from './tools/google/competitor-research/google-historical-domain-rank-overview.tool.js'; 20 | import { GooglePageIntersectionsTool } from './tools/google/competitor-research/google-page-intersection.tool.js'; 21 | import { DataForSeoLabsFilterTool } from './tools/labs-filters.tool.js'; 22 | import { GoogleBulkTrafficEstimationTool } from './tools/google/competitor-research/google-bulk-traffic-estimation.tool.js'; 23 | import { GoogleHistoricalKeywordDataTool } from './tools/google/keyword-research/google-historical-keyword-data.tool.js'; 24 | import { GoogleRelevantPagesTool } from './tools/google/competitor-research/google-relevant-pages.js'; 25 | import { datalabsPrompts } from './dataforseo-labs.prompts.js'; 26 | 27 | export class DataForSEOLabsApi extends BaseModule { 28 | constructor(client: DataForSEOClient) { 29 | super(client); 30 | } 31 | 32 | getTools(): Record<string, ToolDefinition> { 33 | const tools = [ 34 | new GoogleRankedKeywordsTool(this.dataForSEOClient), 35 | new GoogleDomainCompetitorsTool(this.dataForSEOClient), 36 | new GoogleDomainRankOverviewTool(this.dataForSEOClient), 37 | new GoogleKeywordsIdeasTool(this.dataForSEOClient), 38 | new GoogleRelatedKeywordsTool(this.dataForSEOClient), 39 | new GoogleKeywordsSuggestionsTool(this.dataForSEOClient), 40 | new GoogleHistoricalSERP(this.dataForSEOClient), 41 | new GoogleSERPCompetitorsTool(this.dataForSEOClient), 42 | new GoogleBulkKeywordDifficultyTool(this.dataForSEOClient), 43 | new GoogleSubdomainsTool(this.dataForSEOClient), 44 | new GoogleKeywordOverviewTool(this.dataForSEOClient), 45 | new GoogleTopSearchesTool(this.dataForSEOClient), 46 | new GoogleSearchIntentTool(this.dataForSEOClient), 47 | new GoogleKeywordsForSiteTool(this.dataForSEOClient), 48 | new GoogleDomainIntersectionsTool(this.dataForSEOClient), 49 | new GoogleHistoricalDomainRankOverviewTool(this.dataForSEOClient), 50 | new GooglePageIntersectionsTool(this.dataForSEOClient), 51 | new GoogleBulkTrafficEstimationTool(this.dataForSEOClient), 52 | new DataForSeoLabsFilterTool(this.dataForSEOClient), 53 | new GoogleHistoricalKeywordDataTool(this.dataForSEOClient), 54 | new GoogleRelevantPagesTool(this.dataForSEOClient), 55 | // Add more tools here 56 | ]; 57 | 58 | return tools.reduce((acc, tool) => ({ 59 | ...acc, 60 | [tool.getName()]: { 61 | description: tool.getDescription(), 62 | params: tool.getParams(), 63 | handler: (params: any) => { 64 | 65 | return tool.handle(params); 66 | }, 67 | }, 68 | }), {}); 69 | } 70 | 71 | getPrompts(): Record<string, PromptDefinition> { 72 | return datalabsPrompts.reduce((acc, prompt) => ({ 73 | ...acc, 74 | [prompt.name]: { 75 | description: prompt.description, 76 | params: prompt.params, 77 | handler: (params: any) => { 78 | 79 | return prompt.handler(params); 80 | }, 81 | }, 82 | }), {}); 83 | } 84 | } ``` -------------------------------------------------------------------------------- /src/core/modules/dataforseo-labs/tools/google/keyword-research/google-keywords-ideas.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 GoogleKeywordsIdeasTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'dataforseo_labs_google_keyword_ideas'; 12 | } 13 | 14 | getDescription(): string { 15 | return `The Keyword Ideas provides search terms that are relevant to the product or service categories of the specified keywords. The algorithm selects the keywords which fall into the same categories as the seed keywords specified in a POST array. 16 | As a result, you will get a list of relevant keyword ideas for up to 200 seed keywords. 17 | Along with each keyword idea, you will get its search volume rate for the last month, search volume trend for the previous 12 months, as well as current cost-per-click and competition values. Moreover, this endpoint supplies minimum, maximum and average values of daily impressions, clicks and CPC for each result. 18 | `; 19 | } 20 | 21 | getParams(): z.ZodRawShape { 22 | 23 | return { 24 | keywords: z.array(z.string()).describe(`target keywords`), 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 | limit: z.number().min(1).max(1000).default(10).optional().describe("Maximum number of keywords to return"), 36 | offset: z.number().min(0).optional().describe( 37 | `offset in the results array of returned keywords 38 | optional field 39 | default value: 0 40 | if you specify the 10 value, the first ten keywords in the results array will be omitted and the data will be provided for the successive keywords` 41 | ), 42 | filters: this.getFilterExpression().optional().describe( 43 | `you can add several filters at once (8 filters maximum) 44 | you should set a logical operator and, or between the conditions 45 | the following operators are supported: 46 | regex, not_regex, <, <=, >, >=, =, <>, in, not_in, match, not_match, ilike, not_ilike, like, not_like 47 | you can use the % operator with like and not_like, as well as ilike and not_ilike to match any string of zero or more characters 48 | merge operator must be a string and connect two other arrays, availible values: or, and. 49 | example: 50 | ["keyword_info.search_volume",">",0] 51 | [["keyword_info.search_volume","in",[0,1000]],"and",["keyword_info.competition_level","=","LOW"]] 52 | [["keyword_info.search_volume",">",100],"and",[["keyword_info.cpc","<",0.5],"or",["keyword_info.high_top_of_page_bid","<=",0.5]]]` 53 | ), 54 | order_by: z.array(z.string()).optional().describe( 55 | `results sorting rules 56 | optional field 57 | you can use the same values as in the filters array to sort the results 58 | possible sorting types: 59 | asc – results will be sorted in the ascending order 60 | desc – results will be sorted in the descending order 61 | you should use a comma to set up a sorting parameter 62 | default rule: 63 | ["relevance,desc"] 64 | relevance is used as the default sorting rule to provide you with the closest keyword ideas. We recommend using this sorting rule to get highly-relevant search terms. Note that relevance is only our internal system identifier, so it can not be used as a filter, and you will not find this field in the result array. The relevance score is based on a similar principle as used in the Keywords For Keywords endpoint. 65 | note that you can set no more than three sorting rules in a single request 66 | you should use a comma to separate several sorting rules 67 | example: 68 | ["relevance,desc","keyword_info.search_volume,desc"]` 69 | ), 70 | include_clickstream_data: z.boolean().optional().default(false).describe( 71 | `Include or exclude data from clickstream-based metrics in the result`) 72 | }; 73 | } 74 | 75 | async handle(params: any): Promise<any> { 76 | try { 77 | const response = await this.client.makeRequest('/v3/dataforseo_labs/google/keyword_ideas/live', 'POST', [{ 78 | keywords: params.keywords, 79 | location_name: params.location_name, 80 | language_code: params.language_code, 81 | limit: params.limit, 82 | offset: params.offset, 83 | filters: this.formatFilters(params.filters), 84 | order_by: this.formatOrderBy(params.order_by), 85 | include_clickstream_data: params.include_clickstream_data 86 | }]); 87 | return this.validateAndFormatResponse(response); 88 | } catch (error) { 89 | return this.formatErrorResponse(error); 90 | } 91 | } 92 | } ``` -------------------------------------------------------------------------------- /src/core/modules/dataforseo-labs/tools/google/competitor-research/google-relevant-pages.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 GoogleRelevantPagesTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'dataforseo_labs_google_relevant_pages'; 12 | } 13 | 14 | getDescription(): string { 15 | return `This endpoint will provide you with rankings and traffic data for the web pages of the specified domain. You will be able to review each page’s ranking distribution and estimated monthly traffic volume from both organic and paid searches.`; 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 | limit: z.number().min(1).max(1000).default(10).optional().describe("Maximum number of keywords to return"), 34 | offset: z.number().min(0).optional().describe( 35 | `offset in the results array of returned keywords 36 | optional field 37 | default value: 0 38 | if you specify the 10 value, the first ten keywords in the results array will be omitted and the data will be provided for the successive keywords` 39 | ), 40 | filters: this.getFilterExpression().optional().describe( 41 | `you can add several filters at once (8 filters maximum) 42 | you should set a logical operator and, or between the conditions 43 | the following operators are supported: 44 | regex, not_regex, <, <=, >, >=, =, <>, in, not_in, match, not_match, ilike, not_ilike, like, not_like 45 | you can use the % operator with like and not_like, as well as ilike and not_ilike to match any string of zero or more characters 46 | merge operator must be a string and connect two other arrays, availible values: or, and. 47 | example: 48 | ["metrics.organic.count",">",50] 49 | [["metrics.organic.pos_1","<>",0],"and",["metrics.organic.impressions_etv",">=","10"]] 50 | 51 | [[["metrics.organic.count",">=",50],"and",["metrics.organic.pos_1","in",[1,5]]], 52 | "or", 53 | ["metrics.organic.etv",">=","100"]]` 54 | ), 55 | order_by: z.array(z.string()).optional().describe( 56 | `results sorting rules 57 | optional field 58 | you can use the same values as in the filters array to sort the results 59 | possible sorting types: 60 | asc – results will be sorted in the ascending order 61 | desc – results will be sorted in the descending order 62 | you should use a comma to specify a sorting type 63 | example: 64 | ["metrics.paid.etv,asc"] 65 | Note: you can set no more than three sorting rules in a single request 66 | you should use a comma to separate several sorting rules 67 | example: 68 | ["metrics.organic.etv,desc","metrics.paid.count,asc"] 69 | default rule: 70 | ["metrics.organic.count,desc"]` 71 | ), 72 | exclude_top_domains: z.boolean().default(true).describe(`indicates whether to exclude world’s largest websites 73 | optional field 74 | default value: false 75 | set to true if you want to get highly-relevant competitors excluding the top websites`) , 76 | item_types: z.array(z.string()).optional().describe( 77 | `item types to return 78 | optional field 79 | default: ['organic'] 80 | possible values: 81 | organic 82 | paid` 83 | ), 84 | include_clickstream_data: z.boolean().optional().default(false).describe( 85 | `Include or exclude data from clickstream-based metrics in the result`) 86 | 87 | }; 88 | } 89 | 90 | async handle(params: any): Promise<any> { 91 | try { 92 | const response = await this.client.makeRequest('/v3/dataforseo_labs/google/relevant_pages/live', 'POST', [{ 93 | target: params.target, 94 | location_name: params.location_name, 95 | language_code: params.language_code, 96 | ignore_synonyms: params.ignore_synonyms, 97 | filters: this.formatFilters(params.filters), 98 | order_by: this.formatOrderBy(params.order_by), 99 | exclude_top_domains: params.exclude_top_domains, 100 | item_types: params.item_types, 101 | include_clickstream_data: params.include_clickstream_data, 102 | limit: params.limit, 103 | offset: params.offset 104 | }]); 105 | return this.validateAndFormatResponse(response); 106 | } catch (error) { 107 | return this.formatErrorResponse(error); 108 | } 109 | } 110 | } ``` -------------------------------------------------------------------------------- /src/core/modules/base.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { DataForSEOClient } from '../client/dataforseo.client.js'; 3 | import { defaultGlobalToolConfig } from '../config/global.tool.js'; 4 | import { filterFields, parseFieldPaths } from '../utils/field-filter.js'; 5 | import { FieldConfigurationManager } from '../config/field-configuration.js'; 6 | 7 | export interface DataForSEOFullResponse { 8 | version: string; 9 | status_code: number; 10 | status_message: string; 11 | time: string; 12 | cost: number; 13 | tasks_count: number; 14 | tasks_error: number; 15 | tasks: Array<{ 16 | id: string; 17 | status_code: number; 18 | status_message: string; 19 | time: string; 20 | cost: number; 21 | result_count: number; 22 | path: string[]; 23 | data: Record<string, any>; 24 | result: any[]; 25 | }>; 26 | } 27 | 28 | export interface DataForSEOResponse { 29 | id: string; 30 | status_code: number; 31 | status_message: string; 32 | items: any[]; 33 | } 34 | 35 | export abstract class BaseTool { 36 | protected dataForSEOClient: DataForSEOClient; 37 | 38 | constructor(dataForSEOClient: DataForSEOClient) { 39 | this.dataForSEOClient = dataForSEOClient; 40 | } 41 | 42 | protected supportOnlyFullResponse(): boolean { 43 | return false; 44 | } 45 | 46 | protected formatError(error: unknown): string { 47 | return error instanceof Error ? error.message : 'Unknown error'; 48 | } 49 | 50 | protected getFilterExpression(): z.ZodType<any> { 51 | if( defaultGlobalToolConfig.simpleFilter ) { 52 | return z.array(z.any()); 53 | } 54 | const filterExpression = 55 | z.array( 56 | z.union([ 57 | z.array(z.union([z.string(), z.number(), z.boolean()])).length(3), 58 | z.enum(["and", "or"]), 59 | z.array(z.unknown()).length(3), 60 | z.union([z.string(), z.number(),z.unknown()]), 61 | z.any() 62 | ]) 63 | ).max(3); 64 | return filterExpression; 65 | } 66 | 67 | protected validateAndFormatResponse(response: any): { content: Array<{ type: string; text: string }> } { 68 | console.error(JSON.stringify(response)); 69 | if (defaultGlobalToolConfig.fullResponse || this.supportOnlyFullResponse()) { 70 | let data = response as DataForSEOFullResponse; 71 | this.validateResponseFull(data); 72 | let result = data.tasks[0].result; 73 | return this.formatResponse(result); 74 | } 75 | this.validateResponse(response); 76 | return this.formatResponse(response); 77 | } 78 | 79 | protected formatResponse(data: any): { content: Array<{ type: string; text: string }> } { 80 | const fieldConfig = FieldConfigurationManager.getInstance(); 81 | if (fieldConfig.hasConfiguration()) { 82 | const toolName = this.getName(); 83 | if (fieldConfig.isToolConfigured(toolName)) { 84 | const fields = fieldConfig.getFieldsForTool(toolName); 85 | if (fields && fields.length > 0) { 86 | data = filterFields(data, parseFieldPaths(fields)); 87 | } 88 | } 89 | } 90 | return { 91 | content: [ 92 | { 93 | type: "text", 94 | text: JSON.stringify(data, null, 2), 95 | }, 96 | ], 97 | }; 98 | } 99 | 100 | protected formatErrorResponse(error: unknown): { content: Array<{ type: string; text: string }> } { 101 | return { 102 | content: [ 103 | { 104 | type: "text", 105 | text: `Error: ${this.formatError(error)}`, 106 | }, 107 | ], 108 | }; 109 | } 110 | 111 | protected validateResponse(response: DataForSEOResponse): void { 112 | if (response.status_code / 100 !== 200) { 113 | throw new Error(`API Error: ${response.status_message} (Code: ${response.status_code})`); 114 | } 115 | } 116 | 117 | protected validateResponseFull(response: DataForSEOFullResponse): void { 118 | if (response.status_code / 100 !== 200) { 119 | throw new Error(`API Error: ${response.status_message} (Code: ${response.status_code})`); 120 | } 121 | 122 | if (response.tasks.length === 0) { 123 | throw new Error('No tasks in response'); 124 | } 125 | 126 | const task = response.tasks[0]; 127 | if (task.status_code / 100 !== 200) { 128 | throw new Error(`Task Error: ${task.status_message} (Code: ${task.status_code})`); 129 | } 130 | 131 | if (response.tasks_error > 0) { 132 | throw new Error(`Tasks Error: ${response.tasks_error} tasks failed`); 133 | } 134 | } 135 | 136 | abstract getName(): string; 137 | abstract getDescription(): string; 138 | abstract getParams(): z.ZodRawShape; 139 | abstract handle(params: any): Promise<any>; 140 | 141 | protected filterResponseFields(response: any, fields: string[]): any { 142 | if (!fields || fields.length === 0) { 143 | return response; 144 | } 145 | 146 | const fieldPaths = parseFieldPaths(fields); 147 | return filterFields(response, fieldPaths); 148 | } 149 | 150 | protected formatFilters(filters: any[]): any { 151 | if (!filters) 152 | return null; 153 | if (filters.length === 0) { 154 | return null; 155 | } 156 | return this.removeNested(filters); 157 | } 158 | 159 | private removeNested(filters: any[]): any[] { 160 | for (var i = 0; i < filters.length; i++) { 161 | if (Array.isArray(filters[i]) && filters[i].length == 1 && Array.isArray(filters[i][0])) { 162 | filters[i] = this.removeNested(filters[i][0]); 163 | } 164 | } 165 | return filters; 166 | } 167 | 168 | protected formatOrderBy(orderBy: any[]): any { 169 | if (!orderBy) 170 | return null; 171 | if (orderBy.length === 0) { 172 | return null; 173 | } 174 | return orderBy; 175 | } 176 | } 177 | ``` -------------------------------------------------------------------------------- /src/core/modules/dataforseo-labs/tools/google/competitor-research/google-domain-intersection.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 GoogleDomainIntersectionsTool extends BaseTool { 6 | constructor(private client: DataForSEOClient) { 7 | super(client); 8 | } 9 | 10 | getName(): string { 11 | return 'dataforseo_labs_google_domain_intersection'; 12 | } 13 | 14 | getDescription(): string { 15 | return `This endpoint will provide you with the keywords for which both specified domains rank within the same SERP. You will get search volume, competition, cost-per-click and impressions data on each intersecting keyword. Along with that, you will get data on the first and second domain's SERP element discovered for this keyword, as well as the estimated traffic volume and cost of ad traffic.`; 16 | } 17 | 18 | getParams(): z.ZodRawShape { 19 | return { 20 | target1: z.string().describe(`target domain 1`), 21 | target2: z.string().describe(`target domain 2 `), 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 | ignore_synonyms: z.boolean().default(true).describe( 33 | `ignore highly similar keywords, if set to true, results will be more accurate`), 34 | limit: z.number().min(1).max(1000).default(10).optional().describe("Maximum number of keywords to return"), 35 | offset: z.number().min(0).optional().describe( 36 | `offset in the results array of returned keywords 37 | optional field 38 | default value: 0 39 | if you specify the 10 value, the first ten keywords in the results array will be omitted and the data will be provided for the successive keywords` 40 | ), 41 | filters: this.getFilterExpression().optional().describe( 42 | `you can add several filters at once (8 filters maximum) 43 | you should set a logical operator and, or between the conditions 44 | the following operators are supported: 45 | regex, not_regex, <, <=, >, >=, =, <>, in, not_in, match, not_match, ilike, not_ilike, like, not_like 46 | you can use the % operator with like and not_like, as well as ilike and not_ilike to match any string of zero or more characters 47 | merge operator must be a string and connect two other arrays, availible values: or, and. 48 | example: 49 | ["keyword_data.keyword_info.search_volume","in",[100,1000]] 50 | [["first_domain_serp_element.etv",">",0],"and",["first_domain_serp_element.description","like","%goat%"]] 51 | [["keyword_data.keyword_info.search_volume",">",100],"and",[["first_domain_serp_element.description","like","%goat%"],"or",["second_domain_serp_element.type","=","organic"]]]` 52 | ), 53 | order_by: z.array(z.string()).optional().describe( 54 | `results sorting rules 55 | optional field 56 | you can use the same values as in the filters array to sort the results 57 | possible sorting types: 58 | asc – results will be sorted in the ascending order 59 | desc – results will be sorted in the descending order 60 | you should use a comma to set up a sorting parameter 61 | example: 62 | ["keyword_data.keyword_info.competition,desc"] 63 | default rule: 64 | ["keyword_data.keyword_info.search_volume,desc"] 65 | note that you can set no more than three sorting rules in a single request 66 | you should use a comma to separate several sorting rules 67 | example: 68 | ["keyword_data.keyword_info.search_volume,desc","keyword_data.keyword_info.cpc,desc"]` 69 | ), 70 | intersections: z.boolean().optional().describe(`domain intersections in SERP 71 | optional field 72 | if you set intersections to true, you will get the keywords for which both target domains specified as target1 and target2 have results within the same SERP; the corresponding SERP elements for both domains will be provided in the results array 73 | Note: this endpoint will not provide results if the number of intersecting keywords exceeds 10 million 74 | if you specify intersections: false, you will get the keywords for which the domain specified as target1 has results in SERP, and the domain specified as target2 doesn’t; 75 | thus, the corresponding SERP elements and other data will be provided for the domain specified as target1only 76 | default value: true`).default(true), 77 | include_clickstream_data: z.boolean().optional().default(false).describe( 78 | `Include or exclude data from clickstream-based metrics in the result`) 79 | 80 | 81 | }; 82 | } 83 | 84 | async handle(params: any): Promise<any> { 85 | try { 86 | const response = await this.client.makeRequest('/v3/dataforseo_labs/google/domain_intersection/live', 'POST', [{ 87 | target1: params.target1, 88 | target2: params.target2, 89 | location_name: params.location_name, 90 | language_code: params.language_code, 91 | ignore_synonyms: params.ignore_synonyms, 92 | filters: this.formatFilters(params.filters), 93 | order_by: this.formatOrderBy(params.order_by), 94 | exclude_top_domains: params.exclude_top_domains, 95 | item_types: ['organic'], 96 | intersections: params.intersections, 97 | include_clickstream_data: params.include_clickstream_data, 98 | limit: params.limit, 99 | offset: params.offset 100 | }]); 101 | return this.validateAndFormatResponse(response); 102 | } catch (error) { 103 | return this.formatErrorResponse(error); 104 | } 105 | } 106 | } ``` -------------------------------------------------------------------------------- /src/core/modules/dataforseo-labs/tools/labs-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 | [engine: string]: { 13 | [field: string]: string; 14 | }; 15 | }; 16 | } 17 | 18 | export class DataForSeoLabsFilterTool extends BaseTool { 19 | private static cache: ToolFilters | null = null; 20 | private static lastFetchTime: number = 0; 21 | private static readonly CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours in milliseconds 22 | 23 | // Map of tool names to their corresponding filter paths in the API response 24 | private static readonly TOOL_TO_FILTER_MAP: { [key: string]: string } = { 25 | 'dataforseo_labs_google_ranked_keywords': 'ranked_keywords.google', 26 | 'dataforseo_labs_google_keyword_ideas': 'keyword_ideas.google', 27 | 'dataforseo_labs_google_keywords_for_site': 'keywords_for_site.google', 28 | 'dataforseo_labs_google_competitors_domain': 'competitors_domain.google', 29 | 'dataforseo_labs_google_serp_competitors': 'serp_competitors.google', 30 | 'dataforseo_labs_google_subdomains': 'subdomains.google', 31 | 'dataforseo_labs_google_domain_intersection': 'domain_intersection.google', 32 | 'dataforseo_labs_google_page_intersection': 'page_intersection.google', 33 | 'dataforseo_labs_google_historical_serp': 'historical_serp.google', 34 | 'dataforseo_labs_google_historical_rank_overview': 'domain_rank_overview.google', 35 | 'dataforseo_labs_google_relevant_pages': 'relevant_pages.google', 36 | 'dataforseo_labs_google_top_searches': 'top_searches.google', 37 | 'dataforseo_labs_google_keyword_overview': 'keyword_overview.google', 38 | 'dataforseo_labs_google_search_intent': 'search_intent.google', 39 | 'dataforseo_labs_google_bulk_keyword_difficulty': 'bulk_keyword_difficulty.google', 40 | 'dataforseo_labs_google_related_keywords': 'related_keywords.google', 41 | 'dataforseo_labs_google_keyword_suggestions': 'keyword_suggestions.google', 42 | 'dataforseo_labs_google_domain_rank_overview': 'domain_rank_overview.google', 43 | 'dataforseo_labs_google_domain_metrics_by_categories': 'domain_metrics_by_categories.google', 44 | 'dataforseo_labs_google_domain_whois_overview': 'domain_whois_overview.google', 45 | 'dataforseo_labs_google_categories_for_domain': 'categories_for_domain.google', 46 | 'dataforseo_labs_google_keywords_for_categories': 'keywords_for_categories.google', 47 | 'dataforseo_labs_amazon_product_competitors': 'product_competitors.amazon', 48 | 'dataforseo_labs_amazon_product_keyword_intersections': 'product_keyword_intersections.amazon', 49 | 'dataforseo_labs_google_app_competitors': 'app_competitors.google', 50 | 'dataforseo_labs_apple_app_competitors': 'app_competitors.apple', 51 | 'dataforseo_labs_google_app_intersection': 'app_intersection.google', 52 | 'dataforseo_labs_apple_app_intersection': 'app_intersection.apple', 53 | 'dataforseo_labs_google_keywords_for_app': 'keywords_for_app.google', 54 | 'dataforseo_labs_apple_keywords_for_app': 'keywords_for_app.apple', 55 | 'dataforseo_labs_database_rows_count': 'database_rows_count' 56 | }; 57 | 58 | constructor(private client: DataForSEOClient) { 59 | super(client); 60 | } 61 | 62 | getName(): string { 63 | return 'dataforseo_labs_available_filters'; 64 | } 65 | 66 | getDescription(): string { 67 | return `Here you will find all the necessary information about filters that can be used with DataForSEO Labs API endpoints. 68 | 69 | Please, keep in mind that filters are associated with a certain object in the result array, and should be specified accordingly.`; 70 | } 71 | 72 | protected supportOnlyFullResponse(): boolean { 73 | return true; 74 | } 75 | 76 | getParams(): z.ZodRawShape { 77 | return { 78 | tool: z.string().optional().describe('The name of the tool to get filters for') 79 | }; 80 | } 81 | 82 | private async fetchAndCacheFilters(): Promise<ToolFilters> { 83 | const now = Date.now(); 84 | 85 | // Return cached data if it's still valid 86 | if (DataForSeoLabsFilterTool.cache && 87 | (now - DataForSeoLabsFilterTool.lastFetchTime) < DataForSeoLabsFilterTool.CACHE_TTL) { 88 | return DataForSeoLabsFilterTool.cache; 89 | } 90 | 91 | // Fetch fresh data 92 | const response = await this.client.makeRequest('/v3/dataforseo_labs/available_filters', 'GET', null, true) as DataForSEOFullResponse; 93 | this.validateResponseFull(response); 94 | 95 | // Transform the response into our cache format 96 | const filters: ToolFilters = {}; 97 | const result = response.tasks[0].result[0]; 98 | 99 | // Process each tool's filters 100 | for (const [toolName, filterPath] of Object.entries(DataForSeoLabsFilterTool.TOOL_TO_FILTER_MAP)) { 101 | const pathParts = filterPath.split('.'); 102 | let current = result; 103 | 104 | // Navigate to the correct filter object 105 | for (const part of pathParts) { 106 | if (current && current[part]) { 107 | current = current[part]; 108 | } else { 109 | current = null; 110 | break; 111 | } 112 | } 113 | 114 | if (current) { 115 | filters[toolName] = current; 116 | } 117 | } 118 | 119 | // Update cache 120 | DataForSeoLabsFilterTool.cache = filters; 121 | DataForSeoLabsFilterTool.lastFetchTime = now; 122 | 123 | return filters; 124 | } 125 | 126 | async handle(params: any): Promise<any> { 127 | try { 128 | const filters = await this.fetchAndCacheFilters(); 129 | 130 | if (!params.tool) { 131 | return this.formatResponse(filters); 132 | } 133 | 134 | const toolFilters = filters[params.tool]; 135 | if (!toolFilters) { 136 | throw new Error(`No filters found for tool: ${params.tool}`); 137 | } 138 | 139 | return this.formatResponse(toolFilters); 140 | } catch (error) { 141 | return this.formatErrorResponse(error); 142 | } 143 | } 144 | } ``` -------------------------------------------------------------------------------- /src/core/modules/dataforseo-labs/dataforseo-labs.prompts.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { join } from 'path'; 2 | import { z } from 'zod'; 3 | import { PromptDefinition } from '../prompt-definition.js'; 4 | 5 | 6 | export const datalabsPrompts: PromptDefinition[] = [ 7 | { 8 | name: 'create_content_targeting_decision_stage_users', 9 | title: 'Create content targeting decision-stage users', 10 | params: { 11 | product: z.string().describe('The product to search related keywords for') 12 | }, 13 | handler: async (params) => { 14 | return { 15 | messages: [ 16 | { 17 | role: 'user', 18 | content: { 19 | type: 'text', 20 | text: `What are alternative or comparison ("vs", "alternative", "best", "compare") search queries people use for ${params.product}? Return 20 ideas with high search volume.` 21 | } 22 | } 23 | ] 24 | }; 25 | } 26 | }, 27 | { 28 | name: 'generate_seo_friendly_article_ideas', 29 | title: 'Generate SEO-friendly article ideas that directly answer user questions.', 30 | params: { 31 | topic: z.string().describe('topic'), 32 | }, 33 | handler: async (params) => { 34 | return { 35 | messages: [ 36 | { 37 | role: 'user', 38 | content: { 39 | type: 'text', 40 | text: `Show me 20 question-based keywords (what, why, how) for '${params.topic}' with search volume ≥ 300. Include search intent and suggest article headlines that match the query tone.` 41 | } 42 | } 43 | ] 44 | }; 45 | } 46 | }, 47 | { 48 | name: 'focus_on_high_converting_terms_for_paid_campaigns_based_on_buyer_readiness', 49 | title: 'Focus on high-converting terms for paid campaigns based on buyer readiness.', 50 | params: { 51 | product: z.string().describe('The product/service to compare'), 52 | }, 53 | handler: async (params) => { 54 | return { 55 | messages: [ 56 | { 57 | role: 'user', 58 | content: { 59 | type: 'text', 60 | text: `Find 20 commercial and transactional keywords related to '${params.product}'. Filter by CPC ≥ $2 and search volume ≥ 1,000. Suggest landing page angles based on intent.` 61 | } 62 | } 63 | ] 64 | }; 65 | } 66 | }, 67 | { 68 | name: 'structure_site_content_and_internal_linking_based_on_keyword_clusters', 69 | title: 'Structure site content and internal linking based on keyword clusters.', 70 | params: { 71 | keyword: z.string().describe('The keyword to cluster related keywords for'), 72 | }, 73 | handler: async (params) => { 74 | return { 75 | messages: [ 76 | { 77 | role: 'user', 78 | content: { 79 | type: 'text', 80 | text: `Provide a 20-term keyword cluster around ${params.keyword}. Group them by intent (informational, commercial, transactional) and list keyword difficulty and SERP features.` 81 | } 82 | } 83 | ] 84 | }; 85 | } 86 | }, 87 | { 88 | name: 'compare_sites_by_keywords', 89 | title: 'Competitor Comparison', 90 | params: { 91 | site_1: z.string().describe('The first site to compare'), 92 | site_2: z.string().describe('The second site to compare'), 93 | }, 94 | handler: async (params) => { 95 | return { 96 | messages: [ 97 | { 98 | role: 'user', 99 | content: { 100 | type: 'text', 101 | text: `Create a competitor comparison matrix between ${params.site_1} and ${params.site_2} based on keyword overlap and backlink profile.` 102 | } 103 | } 104 | ] 105 | }; 106 | } 107 | }, 108 | { 109 | name: 'build_content_that_aligns_with_user_research_behavior_and_ranks_easier', 110 | title: 'Build content that aligns with user research behavior and ranks easier.', 111 | params: { 112 | topic: z.string().describe('keyword/topic'), 113 | }, 114 | handler: async (params) => { 115 | return { 116 | messages: [ 117 | { 118 | role: 'user', 119 | content: { 120 | type: 'text', 121 | text: `Give me 20 informational keywords around '${params.topic}' with low competition and moderate search volume (≥500). Group them by intent and suggest blog topics for each..` 122 | } 123 | } 124 | ] 125 | }; 126 | } 127 | }, 128 | { 129 | name: 'track_long_term_seo_performance', 130 | title: 'Track long-term SEO performance, visibility shifts, and seasonal trends.', 131 | params: { 132 | domain: z.string().describe('The domain to analyze'), 133 | location: z.string().describe('The location to analyze'), 134 | language: z.string().describe('The language to analyze'), 135 | }, 136 | handler: async (params) => { 137 | return { 138 | messages: [ 139 | { 140 | role: 'user', 141 | content: { 142 | type: 'text', 143 | text: `Using google_historical_rank_overview in DataForSEO Labs API, show how the visibility and SERP position distribution of '${params.domain}' changed in ${params.location} in ${params.language} over the past 12 months. Focus on the top 3, top 10, and top 100 rankings, and highlight any traffic peaks.` 144 | } 145 | } 146 | ] 147 | }; 148 | } 149 | }, 150 | { 151 | name: 'compare_monthly_organic_traffic_trends_and_ranking_distribution_against_a_competitor', 152 | title: 'Compare monthly organic traffic trends and ranking distribution against a competitor.', 153 | params: { 154 | domain: z.string().describe('Your domain to analyze'), 155 | competitor_domain: z.string().describe('Competitor domain to compare against'), 156 | location: z.string().describe('The location to analyze'), 157 | language: z.string().describe('The language to analyze'), 158 | }, 159 | handler: async (params) => { 160 | return { 161 | messages: [ 162 | { 163 | role: 'user', 164 | content: { 165 | type: 'text', 166 | text: `Compare the monthly organic traffic trends and ranking distribution of ${params.domain} vs ${params.competitor_domain} in ${params.location} in ${params.language} using google_domain_rank_overview. Highlight who has better top 10 visibility and estimated traffic this month.` 167 | } 168 | } 169 | ] 170 | }; 171 | } 172 | }, 173 | ]; 174 | ``` -------------------------------------------------------------------------------- /src/main/index-http.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 3 | import { DataForSEOClient, DataForSEOConfig } from '../core/client/dataforseo.client.js'; 4 | import { SerpApiModule } from '../core/modules/serp/serp-api.module.js'; 5 | import { KeywordsDataApiModule } from '../core/modules/keywords-data/keywords-data-api.module.js'; 6 | import { OnPageApiModule } from '../core/modules/onpage/onpage-api.module.js'; 7 | import { DataForSEOLabsApi } from '../core/modules/dataforseo-labs/dataforseo-labs-api.module.js'; 8 | import { EnabledModulesSchema, isModuleEnabled, defaultEnabledModules } from '../core/config/modules.config.js'; 9 | import { BaseModule, ToolDefinition } from '../core/modules/base.module.js'; 10 | import { z } from 'zod'; 11 | import { BacklinksApiModule } from "../core/modules/backlinks/backlinks-api.module.js"; 12 | import { BusinessDataApiModule } from "../core/modules/business-data-api/business-data-api.module.js"; 13 | import { DomainAnalyticsApiModule } from "../core/modules/domain-analytics/domain-analytics-api.module.js"; 14 | import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; 15 | import express, { Request as ExpressRequest, Response, NextFunction } from "express"; 16 | import { randomUUID } from "node:crypto"; 17 | import { GetPromptResult, isInitializeRequest, ReadResourceResult, ServerNotificationSchema } from "@modelcontextprotocol/sdk/types.js" 18 | import { name, version } from '../core/utils/version.js'; 19 | import { ModuleLoaderService } from "../core/utils/module-loader.js"; 20 | import { initializeFieldConfiguration } from '../core/config/field-configuration.js'; 21 | import { initMcpServer } from "./init-mcp-server.js"; 22 | 23 | // Initialize field configuration if provided 24 | initializeFieldConfiguration(); 25 | 26 | // Extended request interface to include auth properties 27 | interface Request extends ExpressRequest { 28 | username?: string; 29 | password?: string; 30 | } 31 | 32 | console.error('Starting DataForSEO MCP Server...'); 33 | console.error(`Server name: ${name}, version: ${version}`); 34 | 35 | function getSessionId() { 36 | return randomUUID().toString(); 37 | } 38 | 39 | async function main() { 40 | const app = express(); 41 | app.use(express.json()); 42 | 43 | // Basic Auth Middleware 44 | const basicAuth = (req: Request, res: Response, next: NextFunction) => { 45 | // Check for Authorization header 46 | const authHeader = req.headers.authorization; 47 | console.error(authHeader) 48 | if (!authHeader || !authHeader.startsWith('Basic ')) { 49 | next(); 50 | return; 51 | } 52 | 53 | // Extract credentials 54 | const base64Credentials = authHeader.split(' ')[1]; 55 | const credentials = Buffer.from(base64Credentials, 'base64').toString('utf-8'); 56 | const [username, password] = credentials.split(':'); 57 | 58 | if (!username || !password) { 59 | console.error('Invalid credentials'); 60 | res.status(401).json({ 61 | jsonrpc: "2.0", 62 | error: { 63 | code: -32001, 64 | message: "Invalid credentials" 65 | }, 66 | id: null 67 | }); 68 | return; 69 | } 70 | 71 | // Add credentials to request 72 | req.username = username; 73 | req.password = password; 74 | next(); 75 | }; 76 | 77 | const handleMcpRequest = async (req: Request, res: Response) => { 78 | // In stateless mode, create a new instance of transport and server for each request 79 | // to ensure complete isolation. A single instance would cause request ID collisions 80 | // when multiple clients connect concurrently. 81 | 82 | try { 83 | 84 | // Check if we have valid credentials 85 | if (!req.username && !req.password) { 86 | // If no request auth, check environment variables 87 | const envUsername = process.env.DATAFORSEO_USERNAME; 88 | const envPassword = process.env.DATAFORSEO_PASSWORD; 89 | if (!envUsername || !envPassword) { 90 | console.error('No DataForSEO credentials provided'); 91 | res.status(401).json({ 92 | jsonrpc: "2.0", 93 | error: { 94 | code: -32001, 95 | message: "Authentication required. Provide DataForSEO credentials." 96 | }, 97 | id: null 98 | }); 99 | return; 100 | } 101 | // Use environment variables 102 | req.username = envUsername; 103 | req.password = envPassword; 104 | } 105 | 106 | const server = initMcpServer(req.username, req.password); 107 | console.error(Date.now().toLocaleString()) 108 | 109 | const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({ 110 | sessionIdGenerator: undefined 111 | }); 112 | 113 | await server.connect(transport); 114 | console.error('handle request'); 115 | await transport.handleRequest(req , res, req.body); 116 | console.error('end handle request'); 117 | req.on('close', () => { 118 | console.error('Request closed'); 119 | transport.close(); 120 | server.close(); 121 | }); 122 | 123 | } catch (error) { 124 | console.error('Error handling MCP request:', error); 125 | if (!res.headersSent) { 126 | res.status(500).json({ 127 | jsonrpc: '2.0', 128 | error: { 129 | code: -32603, 130 | message: 'Internal server error', 131 | }, 132 | id: null, 133 | }); 134 | } 135 | } 136 | }; 137 | 138 | const handleNotAllowed = (method: string) => async (req: Request, res: Response) => { 139 | console.error(`Received ${method} request`); 140 | res.status(405).json({ 141 | jsonrpc: "2.0", 142 | error: { 143 | code: -32000, 144 | message: "Method not allowed." 145 | }, 146 | id: null 147 | }); 148 | }; 149 | 150 | // Apply basic auth and shared handler to both endpoints 151 | app.post('/http', basicAuth, handleMcpRequest); 152 | app.post('/mcp', basicAuth, handleMcpRequest); 153 | 154 | app.get('/http', handleNotAllowed('GET HTTP')); 155 | app.get('/mcp', handleNotAllowed('GET MCP')); 156 | 157 | app.delete('/http', handleNotAllowed('DELETE HTTP')); 158 | app.delete('/mcp', handleNotAllowed('DELETE MCP')); 159 | 160 | // Start the server 161 | const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000; 162 | app.listen(PORT, () => { 163 | console.log(`MCP Stateless Streamable HTTP Server listening on port ${PORT}`); 164 | }); 165 | } 166 | 167 | main().catch((error) => { 168 | console.error("Fatal error in main():", error); 169 | process.exit(1); 170 | }); 171 | ``` -------------------------------------------------------------------------------- /src/core/modules/dataforseo-labs/tools/google/competitor-research/google-page-intersection.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { DataForSEOClient } from '../../../../../client/dataforseo.client.js'; 3 | import { BaseTool } from '../../../../base.tool.js'; 4 | import { mapArrayToNumberedKeys } from '../../../../../utils/map-array-to-numbered-keys.js'; 5 | 6 | export class GooglePageIntersectionsTool extends BaseTool { 7 | constructor(private client: DataForSEOClient) { 8 | super(client); 9 | } 10 | 11 | getName(): string { 12 | return 'dataforseo_labs_google_page_intersection'; 13 | } 14 | 15 | getDescription(): string { 16 | return `This endpoint will provide you with the keywords for which specified pages rank within the same SERP. You will get search volume, competition, cost-per-click and impressions data on each intersecting keyword. Along with that, you will get data on SERP elements that specified pages rank for in search results, as well as the estimated traffic volume and cost of ad traffic. Page Intersection endpoint supports organic, paid, local pack and featured snippet results. 17 | 18 | Find keywords several webpages rank for: 19 | If you would like to get the keywords several pages rank for, you need to specify webpages only in the pages object. This way, you will receive intersected ranked keywords for the specified URLs. 20 | 21 | Find keywords your competitors rank for but you do not: 22 | If you would like to receive all keywords several pages rank for, but particular pages do not, you need to use the exclude_pages array as well. This way you will receive the keywords for which the URLs from the pages object rank for, but the URLs from the exclude_pages array do not`; 23 | } 24 | 25 | getParams(): z.ZodRawShape { 26 | return { 27 | pages: z.array(z.string()).describe(`pages array 28 | required field 29 | you can set up to 20 pages in this object 30 | the pages should be specified with absolute URLs (including http:// or https://) 31 | if you specify a single page here, we will return results only for this page; 32 | you can also use a wildcard ('*') character to specify the search pattern 33 | example: 34 | "example.com" 35 | search for the exact URL 36 | "example.com/eng/*" 37 | search for the example.com page and all its related URLs which start with '/eng/', such as "example.com/eng/index.html" and "example.com/eng/help/", etc. 38 | note: a wilcard should be placed after the slash ('/') character in the end of the URL, it is not possible to place it after the domain in the following way: 39 | https://dataforseo.com* 40 | use https://dataforseo.com/* instead`), 41 | exclude_pages: z.array(z.string()).optional().describe(`URLs of pages you want to exclude 42 | optional field 43 | you can set up to 10 pages in this array 44 | if you use this array, results will contain the keywords for which URLs from the pages object rank, but URLs from exclude_pages array do not; 45 | note that if you specify this field, the results will be based on the keywords any URL from pages ranks for regardless of intersections between them. However, you can set intersection_mode to intersect and results will contain the keywords all URLs from pages rank for in the same SERP and URLs from exclude_pages do not. 46 | use a wildcard (‘*’) character to specify the search pattern 47 | example: 48 | "exclude_pages": [ 49 | "https://www.apple.com/iphone/*", 50 | "https://dataforseo.com/apis/*", 51 | "https://www.microsoft.com/en-us/industry/services/" 52 | ]`), 53 | intersection_mode: z.enum(['union', 'intersect']).optional().describe(`indicates whether to intersect keywords 54 | optional field 55 | use this field to intersect or merge results for the specified URLs 56 | possible values: union, intersect 57 | 58 | union – results are based on all keywords any URL from pages rank for; 59 | 60 | intersect – results are based on the keywords all URLs from pages rank for in the same SERP: 61 | 62 | by default, results are based on the intersect mode if you specify only pages array. If you specify exclude_pages as well, results are based on the union mode`), 63 | location_name: z.string().default("United States").describe(`full name of the location 64 | required field 65 | only in format "Country" (not "City" or "Region") 66 | example: 67 | 'United Kingdom', 'United States', 'Canada'`), 68 | language_code: z.string().default("en").describe( 69 | `language code 70 | required field 71 | example: 72 | en`), 73 | ignore_synonyms: z.boolean().default(true).describe( 74 | `ignore highly similar keywords, if set to true, results will be more accurate`), 75 | limit: z.number().min(1).max(1000).default(10).optional().describe("Maximum number of keywords to return"), 76 | offset: z.number().min(0).optional().describe( 77 | `offset in the results array of returned keywords 78 | optional field 79 | default value: 0 80 | if you specify the 10 value, the first ten keywords in the results array will be omitted and the data will be provided for the successive keywords` 81 | ), 82 | filters: this.getFilterExpression().optional().describe( 83 | `you can add several filters at once (8 filters maximum) 84 | you should set a logical operator and, or between the conditions 85 | the following operators are supported: 86 | regex, not_regex, <, <=, >, >=, =, <>, in, not_in, match, not_match, ilike, not_ilike, like, not_like 87 | you can use the % operator with like and not_like, as well as ilike and not_ilike to match any string of zero or more characters 88 | merge operator must be a string and connect two other arrays, availible values: or, and. 89 | example: 90 | ["keyword_data.keyword_info.search_volume","in",[100,1000]] 91 | [["intersection_result.1.etv",">",0],"and",["intersection_result.2.description","like","%goat%"]] 92 | [["keyword_data.keyword_info.search_volume",">",100],"and",[["intersection_result.1.description","like","%goat%"],"or",["intersection_result.2.type","=","organic"]]]` 93 | ), 94 | order_by: z.array(z.string()).optional().describe( 95 | `results sorting rules 96 | optional field 97 | you can use the same values as in the filters array to sort the results 98 | possible sorting types: 99 | asc – results will be sorted in the ascending order 100 | desc – results will be sorted in the descending order 101 | you should use a comma to set up a sorting parameter 102 | example: 103 | ["keyword_data.keyword_info.competition,desc"] 104 | default rule: 105 | ["keyword_data.keyword_info.search_volume,desc"] 106 | note that you can set no more than three sorting rules in a single request 107 | you should use a comma to separate several sorting rules 108 | example: 109 | ["intersection_result.1.rank_group,asc","intersection_result.2.rank_absolute,asc"]` 110 | ), 111 | include_clickstream_data: z.boolean().optional().default(false).describe( 112 | `Include or exclude data from clickstream-based metrics in the result`) 113 | }; 114 | } 115 | 116 | async handle(params: any): Promise<any> { 117 | try { 118 | const response = await this.client.makeRequest('/v3/dataforseo_labs/google/page_intersection/live', 'POST', [{ 119 | pages: mapArrayToNumberedKeys(params.pages), 120 | location_name: params.location_name, 121 | language_code: params.language_code, 122 | ignore_synonyms: params.ignore_synonyms, 123 | filters: this.formatFilters(params.filters), 124 | order_by: this.formatOrderBy(params.order_by), 125 | exclude_top_domains: params.exclude_top_domains, 126 | item_types: ['organic'], 127 | exclude_pages: params.exclude_pages, 128 | intersection_mode: params.intersection_mode, 129 | limit: params.limit, 130 | offset: params.offset, 131 | include_clickstream_data: params.include_clickstream_data 132 | }]); 133 | return this.validateAndFormatResponse(response); 134 | } catch (error) { 135 | return this.formatErrorResponse(error); 136 | } 137 | } 138 | } ``` -------------------------------------------------------------------------------- /src/main/index-sse-http.ts: -------------------------------------------------------------------------------- ```typescript 1 | import express, { Request as ExpressRequest, Response, NextFunction } from 'express'; 2 | import { randomUUID } from "node:crypto"; 3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 4 | import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; 5 | import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; 6 | import { z } from 'zod'; 7 | import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; 8 | import { DataForSEOClient, DataForSEOConfig } from '../core/client/dataforseo.client.js'; 9 | import { EnabledModulesSchema, isModuleEnabled } from '../core/config/modules.config.js'; 10 | import { BaseModule, ToolDefinition } from '../core/modules/base.module.js'; 11 | import { name, version } from '../core/utils/version.js'; 12 | import { InMemoryEventStore } from '@modelcontextprotocol/sdk/examples/shared/inMemoryEventStore.js'; 13 | import { ModuleLoaderService } from '../core/utils/module-loader.js'; 14 | import { initializeFieldConfiguration } from '../core/config/field-configuration.js'; 15 | import { initMcpServer } from './init-mcp-server.js'; 16 | 17 | // Initialize field configuration if provided 18 | initializeFieldConfiguration(); 19 | console.error('Starting DataForSEO MCP Server...'); 20 | console.error(`Server name: ${name}, version: ${version}`); 21 | 22 | /** 23 | * This example server demonstrates backwards compatibility with both: 24 | * 1. The deprecated HTTP+SSE transport (protocol version 2024-11-05) 25 | * 2. The Streamable HTTP transport (protocol version 2025-03-26) 26 | * 27 | * It maintains a single MCP server instance but exposes two transport options: 28 | * - /mcp: The new Streamable HTTP endpoint (supports GET/POST/DELETE) 29 | * - /sse: The deprecated SSE endpoint for older clients (GET to establish stream) 30 | * - /messages: The deprecated POST endpoint for older clients (POST to send messages) 31 | */ 32 | 33 | // Configuration constants 34 | const CONNECTION_TIMEOUT = 30000; // 30 seconds 35 | const CLEANUP_INTERVAL = 60000; // 1 minute 36 | 37 | // Extended request interface to include auth properties 38 | interface Request extends ExpressRequest { 39 | username?: string; 40 | password?: string; 41 | } 42 | 43 | // Transport interface with timestamp 44 | interface TransportWithTimestamp { 45 | transport: StreamableHTTPServerTransport | SSEServerTransport; 46 | lastActivity: number; 47 | } 48 | 49 | // Store transports by session ID 50 | const transports: Record<string, TransportWithTimestamp> = {}; 51 | 52 | // Cleanup function for stale connections 53 | function cleanupStaleConnections() { 54 | const now = Date.now(); 55 | Object.entries(transports).forEach(([sessionId, { transport, lastActivity }]) => { 56 | if (now - lastActivity > CONNECTION_TIMEOUT) { 57 | console.log(`Cleaning up stale connection for session ${sessionId}`); 58 | try { 59 | transport.close(); 60 | } catch (error) { 61 | console.error(`Error closing transport for session ${sessionId}:`, error); 62 | } 63 | delete transports[sessionId]; 64 | } 65 | }); 66 | } 67 | 68 | // Start periodic cleanup 69 | const cleanupInterval = setInterval(cleanupStaleConnections, CLEANUP_INTERVAL); 70 | 71 | 72 | 73 | // Create Express application 74 | const app = express(); 75 | app.use(express.json()); 76 | 77 | // Basic Auth Middleware 78 | const basicAuth = (req: Request, res: Response, next: NextFunction) => { 79 | const authHeader = req.headers.authorization; 80 | if (!authHeader || !authHeader.startsWith('Basic ')) { 81 | next(); 82 | return; 83 | } 84 | 85 | const base64Credentials = authHeader.split(' ')[1]; 86 | const credentials = Buffer.from(base64Credentials, 'base64').toString('utf-8'); 87 | const [username, password] = credentials.split(':'); 88 | 89 | if (!username || !password) { 90 | console.error('Invalid credentials'); 91 | res.status(401).json({ 92 | jsonrpc: "2.0", 93 | error: { 94 | code: -32001, 95 | message: "Invalid credentials" 96 | }, 97 | id: null 98 | }); 99 | return; 100 | } 101 | 102 | req.username = username; 103 | req.password = password; 104 | next(); 105 | }; 106 | 107 | //============================================================================= 108 | // STREAMABLE HTTP TRANSPORT (PROTOCOL VERSION 2025-03-26) 109 | //============================================================================= 110 | 111 | const handleMcpRequest = async (req: Request, res: Response) => { 112 | // In stateless mode, create a new instance of transport and server for each request 113 | // to ensure complete isolation. A single instance would cause request ID collisions 114 | // when multiple clients connect concurrently. 115 | 116 | try { 117 | console.error(Date.now().toLocaleString()) 118 | 119 | // Handle credentials 120 | if (!req.username && !req.password) { 121 | const envUsername = process.env.DATAFORSEO_USERNAME; 122 | const envPassword = process.env.DATAFORSEO_PASSWORD; 123 | if (!envUsername || !envPassword) { 124 | console.error('No DataForSEO credentials provided'); 125 | res.status(401).json({ 126 | jsonrpc: "2.0", 127 | error: { 128 | code: -32001, 129 | message: "Authentication required. Provide DataForSEO credentials." 130 | }, 131 | id: null 132 | }); 133 | return; 134 | } 135 | req.username = envUsername; 136 | req.password = envPassword; 137 | } 138 | 139 | const server = initMcpServer(req.username, req.password); 140 | console.error(Date.now().toLocaleString()) 141 | 142 | const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({ 143 | sessionIdGenerator: undefined 144 | }); 145 | 146 | await server.connect(transport); 147 | console.error('handle request'); 148 | await transport.handleRequest(req , res, req.body); 149 | console.error('end handle request'); 150 | req.on('close', () => { 151 | console.error('Request closed'); 152 | transport.close(); 153 | server.close(); 154 | }); 155 | 156 | } catch (error) { 157 | console.error('Error handling MCP request:', error); 158 | if (!res.headersSent) { 159 | res.status(500).json({ 160 | jsonrpc: '2.0', 161 | error: { 162 | code: -32603, 163 | message: 'Internal server error', 164 | }, 165 | id: null, 166 | }); 167 | } 168 | } 169 | }; 170 | 171 | const handleNotAllowed = (method: string) => async (req: Request, res: Response) => { 172 | console.error(`Received ${method} request`); 173 | res.status(405).json({ 174 | jsonrpc: "2.0", 175 | error: { 176 | code: -32000, 177 | message: "Method not allowed." 178 | }, 179 | id: null 180 | }); 181 | }; 182 | 183 | // Apply basic auth and shared handler to both endpoints 184 | app.post('/http', basicAuth, handleMcpRequest); 185 | app.post('/mcp', basicAuth, handleMcpRequest); 186 | 187 | app.get('/http', handleNotAllowed('GET HTTP')); 188 | app.get('/mcp', handleNotAllowed('GET MCP')); 189 | 190 | app.delete('/http', handleNotAllowed('DELETE HTTP')); 191 | app.delete('/mcp', handleNotAllowed('DELETE MCP')); 192 | 193 | //============================================================================= 194 | // DEPRECATED HTTP+SSE TRANSPORT (PROTOCOL VERSION 2024-11-05) 195 | //============================================================================= 196 | 197 | app.get('/sse', basicAuth, async (req: Request, res: Response) => { 198 | console.log('Received GET request to /sse (deprecated SSE transport)'); 199 | 200 | // Handle credentials 201 | if (!req.username && !req.password) { 202 | const envUsername = process.env.DATAFORSEO_USERNAME; 203 | const envPassword = process.env.DATAFORSEO_PASSWORD; 204 | 205 | if (!envUsername || !envPassword) { 206 | console.error('No DataForSEO credentials provided'); 207 | res.status(401).json({ 208 | jsonrpc: "2.0", 209 | error: { 210 | code: -32001, 211 | message: "Authentication required. Provide DataForSEO credentials." 212 | }, 213 | id: null 214 | }); 215 | return; 216 | } 217 | req.username = envUsername; 218 | req.password = envPassword; 219 | } 220 | 221 | const transport = new SSEServerTransport('/messages', res); 222 | 223 | // Store transport with timestamp 224 | transports[transport.sessionId] = { 225 | transport, 226 | lastActivity: Date.now() 227 | }; 228 | 229 | // Handle connection cleanup 230 | const cleanup = () => { 231 | try { 232 | transport.close(); 233 | } catch (error) { 234 | console.error(`Error closing transport for session ${transport.sessionId}:`, error); 235 | } 236 | delete transports[transport.sessionId]; 237 | }; 238 | 239 | res.on("error", cleanup); 240 | req.on("error", cleanup); 241 | req.socket.on("error", cleanup); 242 | req.socket.on("timeout", cleanup); 243 | 244 | // Set socket timeout 245 | req.socket.setTimeout(CONNECTION_TIMEOUT); 246 | 247 | const server = initMcpServer(req.username, req.password); 248 | await server.connect(transport); 249 | }); 250 | 251 | app.post("/messages", basicAuth, async (req: Request, res: Response) => { 252 | const sessionId = req.query.sessionId as string; 253 | 254 | // Handle credentials 255 | if (!req.username && !req.password) { 256 | const envUsername = process.env.DATAFORSEO_USERNAME; 257 | const envPassword = process.env.DATAFORSEO_PASSWORD; 258 | 259 | if (!envUsername || !envPassword) { 260 | res.status(401).json({ 261 | jsonrpc: "2.0", 262 | error: { 263 | code: -32001, 264 | message: "Authentication required. Provide DataForSEO credentials." 265 | }, 266 | id: null 267 | }); 268 | return; 269 | } 270 | req.username = envUsername; 271 | req.password = envPassword; 272 | } 273 | 274 | const transportData = transports[sessionId]; 275 | if (!transportData) { 276 | res.status(400).send('No transport found for sessionId'); 277 | return; 278 | } 279 | 280 | if (!(transportData.transport instanceof SSEServerTransport)) { 281 | res.status(400).json({ 282 | jsonrpc: '2.0', 283 | error: { 284 | code: -32000, 285 | message: 'Bad Request: Session exists but uses a different transport protocol', 286 | }, 287 | id: null, 288 | }); 289 | return; 290 | } 291 | 292 | // Update last activity timestamp 293 | transportData.lastActivity = Date.now(); 294 | 295 | await transportData.transport.handlePostMessage(req, res, req.body); 296 | }); 297 | 298 | // Start the server 299 | const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000; 300 | const server = app.listen(PORT, () => { 301 | console.log(`DataForSEO MCP Server with SSE compatibility listening on port ${PORT}`); 302 | console.log(` 303 | ============================================== 304 | SUPPORTED TRANSPORT OPTIONS: 305 | 306 | 1. Streamable Http (Protocol version: 2025-03-26) 307 | Endpoint: /http (POST) 308 | Endpoint: /mcp (POST) 309 | 310 | 311 | 2. Http + SSE (Protocol version: 2024-11-05) 312 | Endpoints: /sse (GET) and /messages (POST) 313 | Usage: 314 | - Establish SSE stream with GET to /sse 315 | - Send requests with POST to /messages?sessionId=<id> 316 | ============================================== 317 | `); 318 | }); 319 | 320 | // Handle server shutdown 321 | process.on('SIGINT', async () => { 322 | console.log('Shutting down server...'); 323 | 324 | // Clear cleanup interval 325 | clearInterval(cleanupInterval); 326 | 327 | // Close HTTP server 328 | server.close(); 329 | 330 | // Close all active transports 331 | for (const sessionId in transports) { 332 | try { 333 | console.log(`Closing transport for session ${sessionId}`); 334 | await transports[sessionId].transport.close(); 335 | delete transports[sessionId]; 336 | } catch (error) { 337 | console.error(`Error closing transport for session ${sessionId}:`, error); 338 | } 339 | } 340 | console.log('Server shutdown complete'); 341 | process.exit(0); 342 | }); ```