#
tokens: 47599/50000 34/110 files (page 2/4)
lines: on (toggle) GitHub
raw markdown copy reset
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 | });
```
Page 2/4FirstPrevNextLast