@yoryoboy/bi-mcp 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +38 -427
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/mcp-use.json +2 -2
  4. package/dist/src/config/profile-store.js +4 -2
  5. package/dist/src/config/profile-store.js.map +2 -2
  6. package/dist/src/services/search-console/search-console-utils.js +275 -0
  7. package/dist/src/services/search-console/search-console-utils.js.map +7 -0
  8. package/dist/src/tools/config/list-profiles.js +1 -1
  9. package/dist/src/tools/config/list-profiles.js.map +2 -2
  10. package/dist/src/tools/search-console/country-breakdown.js +1 -1
  11. package/dist/src/tools/search-console/country-breakdown.js.map +1 -1
  12. package/dist/src/tools/search-console/device-breakdown.js +1 -1
  13. package/dist/src/tools/search-console/device-breakdown.js.map +1 -1
  14. package/dist/src/tools/search-console/high-impression-low-click-queries.js +1 -1
  15. package/dist/src/tools/search-console/high-impression-low-click-queries.js.map +1 -1
  16. package/dist/src/tools/search-console/list-accessible-sites.js +1 -1
  17. package/dist/src/tools/search-console/list-accessible-sites.js.map +1 -1
  18. package/dist/src/tools/search-console/low-ctr-opportunities.js +1 -1
  19. package/dist/src/tools/search-console/low-ctr-opportunities.js.map +1 -1
  20. package/dist/src/tools/search-console/page-performance.js +1 -1
  21. package/dist/src/tools/search-console/page-performance.js.map +1 -1
  22. package/dist/src/tools/search-console/product-demand-low-capture-queries.js +1 -1
  23. package/dist/src/tools/search-console/product-demand-low-capture-queries.js.map +1 -1
  24. package/dist/src/tools/search-console/query-page-matrix.js +1 -1
  25. package/dist/src/tools/search-console/query-page-matrix.js.map +1 -1
  26. package/dist/src/tools/search-console/query-performance.js +1 -1
  27. package/dist/src/tools/search-console/query-performance.js.map +1 -1
  28. package/dist/src/tools/search-console/quick-win-opportunities.js +1 -1
  29. package/dist/src/tools/search-console/quick-win-opportunities.js.map +1 -1
  30. package/dist/src/tools/search-console/rising-non-brand-queries.js +1 -1
  31. package/dist/src/tools/search-console/rising-non-brand-queries.js.map +1 -1
  32. package/dist/src/tools/search-console/search-performance.js +1 -1
  33. package/dist/src/tools/search-console/search-performance.js.map +1 -1
  34. package/dist/src/tools/search-console/site-context.js +1 -1
  35. package/dist/src/tools/search-console/site-context.js.map +1 -1
  36. package/dist/src/tools/search-console/visibility-declines.js +1 -1
  37. package/dist/src/tools/search-console/visibility-declines.js.map +1 -1
  38. package/dist/src/tools/vtex/profile-resolution.js +2 -2
  39. package/dist/src/tools/vtex/profile-resolution.js.map +2 -2
  40. package/package.json +1 -1
package/dist/mcp-use.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "includeInspector": false,
3
- "buildTime": "2026-04-16T23:30:11.314Z",
4
- "buildId": "d870ef9eeeccfafb",
3
+ "buildTime": "2026-04-29T14:52:12.499Z",
4
+ "buildId": "69c12ab1e81d4770",
5
5
  "entryPoint": "dist/index.js",
6
6
  "widgets": {}
7
7
  }
@@ -36,13 +36,15 @@ async function upsertProfile(input) {
36
36
  );
37
37
  return mapProfileRow(result.rows[0]);
38
38
  }
39
- async function listProfiles() {
39
+ async function listProfiles(options = {}) {
40
40
  const result = await getDb().query(
41
41
  `
42
42
  select id, name, is_active, created_at, updated_at
43
43
  from profiles
44
+ where ($1::boolean = false or is_active = true)
44
45
  order by id asc
45
- `
46
+ `,
47
+ [options.activeOnly ?? false]
46
48
  );
47
49
  return result.rows.map(mapProfileRow);
48
50
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/config/profile-store.ts"],
4
- "sourcesContent": ["import { getDb } from \"../db/client.js\";\n\nexport interface ProfileRecord {\n id: string;\n name: string;\n isActive: boolean;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface DeletedProfileRecord {\n id: string;\n name: string;\n isActive: boolean;\n deletedAt: Date;\n}\n\ninterface ProfileRow {\n id: string;\n name: string;\n is_active: boolean;\n created_at: Date;\n updated_at: Date;\n}\n\nfunction mapProfileRow(row: ProfileRow): ProfileRecord {\n return {\n id: row.id,\n name: row.name,\n isActive: row.is_active,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n };\n}\n\nexport async function getProfile(profileId: string): Promise<ProfileRecord | null> {\n const result = await getDb().query<ProfileRow>(\n `\n select id, name, is_active, created_at, updated_at\n from profiles\n where id = $1\n `,\n [profileId]\n );\n\n const row = result.rows[0];\n return row ? mapProfileRow(row) : null;\n}\n\nexport async function upsertProfile(input: {\n id: string;\n name: string;\n isActive: boolean;\n}): Promise<ProfileRecord> {\n const result = await getDb().query<ProfileRow>(\n `\n insert into profiles (id, name, is_active)\n values ($1, $2, $3)\n on conflict (id) do update\n set\n name = excluded.name,\n is_active = excluded.is_active,\n updated_at = now()\n returning id, name, is_active, created_at, updated_at\n `,\n [input.id, input.name, input.isActive]\n );\n\n return mapProfileRow(result.rows[0]);\n}\n\nexport async function listProfiles(): Promise<ProfileRecord[]> {\n const result = await getDb().query<ProfileRow>(\n `\n select id, name, is_active, created_at, updated_at\n from profiles\n order by id asc\n `\n );\n\n return result.rows.map(mapProfileRow);\n}\n\nexport async function deleteProfile(profileId: string): Promise<DeletedProfileRecord | null> {\n const result = await getDb().query<ProfileRow>(\n `\n delete from profiles\n where id = $1\n returning id, name, is_active, created_at, updated_at\n `,\n [profileId]\n );\n\n const row = result.rows[0];\n if (!row) {\n return null;\n }\n\n return {\n id: row.id,\n name: row.name,\n isActive: row.is_active,\n deletedAt: new Date(),\n };\n}\n\nexport async function assertActiveProfile(profileId: string): Promise<ProfileRecord> {\n const profile = await getProfile(profileId);\n\n if (!profile) {\n throw new Error(`Profile not found: \"${profileId}\"`);\n }\n\n if (!profile.isActive) {\n throw new Error(`Profile \"${profileId}\" is disabled`);\n }\n\n return profile;\n}\n"],
5
- "mappings": "AAAA,SAAS,aAAa;AAyBtB,SAAS,cAAc,KAAgC;AACrD,SAAO;AAAA,IACL,IAAI,IAAI;AAAA,IACR,MAAM,IAAI;AAAA,IACV,UAAU,IAAI;AAAA,IACd,WAAW,IAAI;AAAA,IACf,WAAW,IAAI;AAAA,EACjB;AACF;AAEA,eAAsB,WAAW,WAAkD;AACjF,QAAM,SAAS,MAAM,MAAM,EAAE;AAAA,IAC3B;AAAA;AAAA;AAAA;AAAA;AAAA,IAKA,CAAC,SAAS;AAAA,EACZ;AAEA,QAAM,MAAM,OAAO,KAAK,CAAC;AACzB,SAAO,MAAM,cAAc,GAAG,IAAI;AACpC;AAEA,eAAsB,cAAc,OAIT;AACzB,QAAM,SAAS,MAAM,MAAM,EAAE;AAAA,IAC3B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAUA,CAAC,MAAM,IAAI,MAAM,MAAM,MAAM,QAAQ;AAAA,EACvC;AAEA,SAAO,cAAc,OAAO,KAAK,CAAC,CAAC;AACrC;AAEA,eAAsB,eAAyC;AAC7D,QAAM,SAAS,MAAM,MAAM,EAAE;AAAA,IAC3B;AAAA;AAAA;AAAA;AAAA;AAAA,EAKF;AAEA,SAAO,OAAO,KAAK,IAAI,aAAa;AACtC;AAEA,eAAsB,cAAc,WAAyD;AAC3F,QAAM,SAAS,MAAM,MAAM,EAAE;AAAA,IAC3B;AAAA;AAAA;AAAA;AAAA;AAAA,IAKA,CAAC,SAAS;AAAA,EACZ;AAEA,QAAM,MAAM,OAAO,KAAK,CAAC;AACzB,MAAI,CAAC,KAAK;AACR,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,IAAI,IAAI;AAAA,IACR,MAAM,IAAI;AAAA,IACV,UAAU,IAAI;AAAA,IACd,WAAW,oBAAI,KAAK;AAAA,EACtB;AACF;AAEA,eAAsB,oBAAoB,WAA2C;AACnF,QAAM,UAAU,MAAM,WAAW,SAAS;AAE1C,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,uBAAuB,SAAS,GAAG;AAAA,EACrD;AAEA,MAAI,CAAC,QAAQ,UAAU;AACrB,UAAM,IAAI,MAAM,YAAY,SAAS,eAAe;AAAA,EACtD;AAEA,SAAO;AACT;",
4
+ "sourcesContent": ["import { getDb } from \"../db/client.js\";\n\nexport interface ProfileRecord {\n id: string;\n name: string;\n isActive: boolean;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface DeletedProfileRecord {\n id: string;\n name: string;\n isActive: boolean;\n deletedAt: Date;\n}\n\ninterface ProfileRow {\n id: string;\n name: string;\n is_active: boolean;\n created_at: Date;\n updated_at: Date;\n}\n\nfunction mapProfileRow(row: ProfileRow): ProfileRecord {\n return {\n id: row.id,\n name: row.name,\n isActive: row.is_active,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n };\n}\n\nexport async function getProfile(profileId: string): Promise<ProfileRecord | null> {\n const result = await getDb().query<ProfileRow>(\n `\n select id, name, is_active, created_at, updated_at\n from profiles\n where id = $1\n `,\n [profileId]\n );\n\n const row = result.rows[0];\n return row ? mapProfileRow(row) : null;\n}\n\nexport async function upsertProfile(input: {\n id: string;\n name: string;\n isActive: boolean;\n}): Promise<ProfileRecord> {\n const result = await getDb().query<ProfileRow>(\n `\n insert into profiles (id, name, is_active)\n values ($1, $2, $3)\n on conflict (id) do update\n set\n name = excluded.name,\n is_active = excluded.is_active,\n updated_at = now()\n returning id, name, is_active, created_at, updated_at\n `,\n [input.id, input.name, input.isActive]\n );\n\n return mapProfileRow(result.rows[0]);\n}\n\nexport async function listProfiles(options: { activeOnly?: boolean } = {}): Promise<ProfileRecord[]> {\n const result = await getDb().query<ProfileRow>(\n `\n select id, name, is_active, created_at, updated_at\n from profiles\n where ($1::boolean = false or is_active = true)\n order by id asc\n `,\n [options.activeOnly ?? false]\n );\n\n return result.rows.map(mapProfileRow);\n}\n\nexport async function deleteProfile(profileId: string): Promise<DeletedProfileRecord | null> {\n const result = await getDb().query<ProfileRow>(\n `\n delete from profiles\n where id = $1\n returning id, name, is_active, created_at, updated_at\n `,\n [profileId]\n );\n\n const row = result.rows[0];\n if (!row) {\n return null;\n }\n\n return {\n id: row.id,\n name: row.name,\n isActive: row.is_active,\n deletedAt: new Date(),\n };\n}\n\nexport async function assertActiveProfile(profileId: string): Promise<ProfileRecord> {\n const profile = await getProfile(profileId);\n\n if (!profile) {\n throw new Error(`Profile not found: \"${profileId}\"`);\n }\n\n if (!profile.isActive) {\n throw new Error(`Profile \"${profileId}\" is disabled`);\n }\n\n return profile;\n}\n"],
5
+ "mappings": "AAAA,SAAS,aAAa;AAyBtB,SAAS,cAAc,KAAgC;AACrD,SAAO;AAAA,IACL,IAAI,IAAI;AAAA,IACR,MAAM,IAAI;AAAA,IACV,UAAU,IAAI;AAAA,IACd,WAAW,IAAI;AAAA,IACf,WAAW,IAAI;AAAA,EACjB;AACF;AAEA,eAAsB,WAAW,WAAkD;AACjF,QAAM,SAAS,MAAM,MAAM,EAAE;AAAA,IAC3B;AAAA;AAAA;AAAA;AAAA;AAAA,IAKA,CAAC,SAAS;AAAA,EACZ;AAEA,QAAM,MAAM,OAAO,KAAK,CAAC;AACzB,SAAO,MAAM,cAAc,GAAG,IAAI;AACpC;AAEA,eAAsB,cAAc,OAIT;AACzB,QAAM,SAAS,MAAM,MAAM,EAAE;AAAA,IAC3B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAUA,CAAC,MAAM,IAAI,MAAM,MAAM,MAAM,QAAQ;AAAA,EACvC;AAEA,SAAO,cAAc,OAAO,KAAK,CAAC,CAAC;AACrC;AAEA,eAAsB,aAAa,UAAoC,CAAC,GAA6B;AACnG,QAAM,SAAS,MAAM,MAAM,EAAE;AAAA,IAC3B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA,CAAC,QAAQ,cAAc,KAAK;AAAA,EAC9B;AAEA,SAAO,OAAO,KAAK,IAAI,aAAa;AACtC;AAEA,eAAsB,cAAc,WAAyD;AAC3F,QAAM,SAAS,MAAM,MAAM,EAAE;AAAA,IAC3B;AAAA;AAAA;AAAA;AAAA;AAAA,IAKA,CAAC,SAAS;AAAA,EACZ;AAEA,QAAM,MAAM,OAAO,KAAK,CAAC;AACzB,MAAI,CAAC,KAAK;AACR,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,IAAI,IAAI;AAAA,IACR,MAAM,IAAI;AAAA,IACV,UAAU,IAAI;AAAA,IACd,WAAW,oBAAI,KAAK;AAAA,EACtB;AACF;AAEA,eAAsB,oBAAoB,WAA2C;AACnF,QAAM,UAAU,MAAM,WAAW,SAAS;AAE1C,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,uBAAuB,SAAS,GAAG;AAAA,EACrD;AAEA,MAAI,CAAC,QAAQ,UAAU;AACrB,UAAM,IAAI,MAAM,YAAY,SAAS,eAAe;AAAA,EACtD;AAEA,SAAO;AACT;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,275 @@
1
+ import { z } from "zod";
2
+ const searchConsoleDateRegex = /^\d{4}-\d{2}-\d{2}$/;
3
+ const searchConsoleSearchTypes = ["web", "image", "video", "news", "discover"];
4
+ const searchConsoleDimensions = [
5
+ "query",
6
+ "page",
7
+ "country",
8
+ "device",
9
+ "date",
10
+ "searchAppearance"
11
+ ];
12
+ const searchConsoleFilterOperators = [
13
+ "contains",
14
+ "equals",
15
+ "notContains",
16
+ "notEquals",
17
+ "includingRegex",
18
+ "excludingRegex"
19
+ ];
20
+ const searchConsoleAggregationTypes = ["auto", "byPage", "byProperty"];
21
+ const searchConsoleDimensionFilterSchema = z.object({
22
+ dimension: z.enum(searchConsoleDimensions).describe("Dimension to filter on in Search Console (query, page, country, device, date, or searchAppearance)"),
23
+ operator: z.enum(searchConsoleFilterOperators).describe("Filter operator supported by Search Console."),
24
+ expression: z.string().min(1).describe("Filter value or regex expression.")
25
+ });
26
+ const siteUrlSchemaField = z.string().optional().describe(
27
+ "Search Console site URL or domain property identifier (for example https://www.example.com/ or sc-domain:example.com). If omitted, the tool expects the site to be configured elsewhere."
28
+ );
29
+ const profileIdSchemaField = z.string().optional().describe("Optional business profile identifier for future multi-profile resolution. Defaults to the active profile when supported.");
30
+ const startDateSchemaField = z.string().regex(searchConsoleDateRegex).describe("Start date in YYYY-MM-DD format.");
31
+ const endDateSchemaField = z.string().regex(searchConsoleDateRegex).describe("End date in YYYY-MM-DD format.");
32
+ const currentStartDateSchemaField = z.string().regex(searchConsoleDateRegex).describe("Current comparison period start date in YYYY-MM-DD format.");
33
+ const currentEndDateSchemaField = z.string().regex(searchConsoleDateRegex).describe("Current comparison period end date in YYYY-MM-DD format.");
34
+ const previousStartDateSchemaField = z.string().regex(searchConsoleDateRegex).describe("Previous comparison period start date in YYYY-MM-DD format.");
35
+ const previousEndDateSchemaField = z.string().regex(searchConsoleDateRegex).describe("Previous comparison period end date in YYYY-MM-DD format.");
36
+ const rowLimitSchemaField = z.number().int().min(1).max(25e3).optional().describe("Maximum number of rows to return. Search Console supports up to 25,000 rows per request.");
37
+ const startRowSchemaField = z.number().int().min(0).optional().describe("Zero-based row offset for paginated Search Console requests.");
38
+ const searchTypeSchemaField = z.enum(searchConsoleSearchTypes).optional().describe("Search type to query. Defaults to web.");
39
+ const brandTermsSchemaField = z.array(z.string().min(1)).max(50).optional().describe("Brand tokens used to separate branded from non-branded queries.");
40
+ function resolveSearchConsoleSiteUrl(siteUrl, profileId) {
41
+ const explicitSiteUrl = siteUrl?.trim();
42
+ if (explicitSiteUrl) {
43
+ return explicitSiteUrl;
44
+ }
45
+ if (profileId?.trim()) {
46
+ throw new Error(
47
+ `Missing Search Console site URL. profileId "${profileId}" was provided, but business-data based site resolution is not implemented yet. Pass siteUrl explicitly or configure Search Console site resolution first.`
48
+ );
49
+ }
50
+ throw new Error(
51
+ "Missing Search Console site URL. Pass siteUrl explicitly or first call gsc_list_accessible_sites to discover available properties."
52
+ );
53
+ }
54
+ function inferSearchConsolePropertyType(siteUrl) {
55
+ if (siteUrl.startsWith("sc-domain:")) {
56
+ return "domain";
57
+ }
58
+ if (/^https?:\/\//.test(siteUrl)) {
59
+ return "url_prefix";
60
+ }
61
+ return "unknown";
62
+ }
63
+ function normalizePermissionLevel(permissionLevel) {
64
+ return permissionLevel?.trim().toLowerCase() || void 0;
65
+ }
66
+ function normalizeSearchConsoleSiteEntry(siteEntry) {
67
+ return {
68
+ site_url: siteEntry.siteUrl,
69
+ property_type: inferSearchConsolePropertyType(siteEntry.siteUrl),
70
+ permission_level: normalizePermissionLevel(siteEntry.permissionLevel)
71
+ };
72
+ }
73
+ function toPercent(numerator, denominator) {
74
+ if (!Number.isFinite(numerator) || !Number.isFinite(denominator) || denominator <= 0) {
75
+ return 0;
76
+ }
77
+ return numerator / denominator * 100;
78
+ }
79
+ function round(value, decimals = 2) {
80
+ if (!Number.isFinite(value)) {
81
+ return 0;
82
+ }
83
+ const factor = 10 ** decimals;
84
+ return Math.round(value * factor) / factor;
85
+ }
86
+ function normalizeSearchConsoleRow(row, dimensions = []) {
87
+ const clicks = row.clicks ?? 0;
88
+ const impressions = row.impressions ?? 0;
89
+ const ctr = typeof row.ctr === "number" ? row.ctr * 100 : toPercent(clicks, impressions);
90
+ const keys = row.keys ?? [];
91
+ return {
92
+ keys,
93
+ dimensions: dimensions.reduce((accumulator, dimension, index) => {
94
+ accumulator[dimension] = keys[index] ?? "";
95
+ return accumulator;
96
+ }, {}),
97
+ clicks: round(clicks, 2),
98
+ impressions: round(impressions, 2),
99
+ ctr_percent: round(ctr, 2),
100
+ position: round(row.position ?? 0, 2)
101
+ };
102
+ }
103
+ function buildSearchConsoleQueryBody(input) {
104
+ const dimensionFilterGroups = input.dimensionFilters && input.dimensionFilters.length > 0 ? [
105
+ {
106
+ groupType: "and",
107
+ filters: input.dimensionFilters
108
+ }
109
+ ] : void 0;
110
+ return {
111
+ startDate: input.startDate,
112
+ endDate: input.endDate,
113
+ dimensions: input.dimensions,
114
+ type: input.searchType ?? "web",
115
+ dimensionFilterGroups,
116
+ aggregationType: input.aggregationType,
117
+ rowLimit: input.rowLimit,
118
+ startRow: input.startRow
119
+ };
120
+ }
121
+ function buildPaginationMetadata(input) {
122
+ const startRow = input.startRow ?? 0;
123
+ const rowLimit = input.rowLimit;
124
+ const isPartial = Boolean(rowLimit) && input.returnedRows === rowLimit;
125
+ const nextStartRow = isPartial ? startRow + input.returnedRows : void 0;
126
+ return {
127
+ is_partial: isPartial,
128
+ start_row: startRow,
129
+ row_limit: rowLimit,
130
+ returned_rows: input.returnedRows,
131
+ next_start_row: nextStartRow,
132
+ continuation_instructions: nextStartRow ? "This Search Console response may be truncated at row_limit. If you need additional rows, call the same tool again with startRow set to next_start_row." : void 0
133
+ };
134
+ }
135
+ function parseBrandTerms(brandTerms) {
136
+ return (brandTerms ?? []).map((term) => term.trim().toLowerCase()).filter(Boolean);
137
+ }
138
+ function isBrandedQuery(query, brandTerms) {
139
+ const normalizedQuery = query.trim().toLowerCase();
140
+ const normalizedBrandTerms = parseBrandTerms(brandTerms);
141
+ if (!normalizedQuery || normalizedBrandTerms.length === 0) {
142
+ return false;
143
+ }
144
+ return normalizedBrandTerms.some((brandTerm) => normalizedQuery.includes(brandTerm));
145
+ }
146
+ function classifyBrandLabel(query, brandTerms) {
147
+ return isBrandedQuery(query, brandTerms) ? "brand" : "non_brand";
148
+ }
149
+ function computeChangePercent(currentValue, previousValue) {
150
+ if (!Number.isFinite(currentValue) || !Number.isFinite(previousValue)) {
151
+ return null;
152
+ }
153
+ if (previousValue === 0) {
154
+ if (currentValue === 0) {
155
+ return 0;
156
+ }
157
+ return null;
158
+ }
159
+ return round((currentValue - previousValue) / previousValue * 100, 2);
160
+ }
161
+ function sumSearchConsoleRows(rows) {
162
+ const totals = rows.reduce(
163
+ (accumulator, row) => {
164
+ accumulator.clicks += row.clicks;
165
+ accumulator.impressions += row.impressions;
166
+ accumulator.weightedPosition += row.position * row.impressions;
167
+ return accumulator;
168
+ },
169
+ {
170
+ clicks: 0,
171
+ impressions: 0,
172
+ weightedPosition: 0
173
+ }
174
+ );
175
+ return {
176
+ clicks: round(totals.clicks, 2),
177
+ impressions: round(totals.impressions, 2),
178
+ ctrPercent: round(toPercent(totals.clicks, totals.impressions), 2),
179
+ averagePosition: totals.impressions > 0 ? round(totals.weightedPosition / totals.impressions, 2) : 0
180
+ };
181
+ }
182
+ function buildComparisonMap(rows, keyBuilder) {
183
+ return new Map(rows.map((row) => [keyBuilder(row), row]));
184
+ }
185
+ function tokenizeQuery(query) {
186
+ return query.toLowerCase().split(/[^a-z0-9áéíóúüñ]+/i).map((token) => token.trim()).filter(Boolean);
187
+ }
188
+ function looksLikeCommercialQuery(query) {
189
+ const tokens = tokenizeQuery(query);
190
+ const commercialTerms = /* @__PURE__ */ new Set([
191
+ "comprar",
192
+ "precio",
193
+ "precios",
194
+ "cuanto",
195
+ "oferta",
196
+ "ofertas",
197
+ "envio",
198
+ "shop",
199
+ "tienda",
200
+ "modelo",
201
+ "medida",
202
+ "ml",
203
+ "cm",
204
+ "kg"
205
+ ]);
206
+ return tokens.some((token) => commercialTerms.has(token));
207
+ }
208
+ function looksLikeProductQuery(query, productTerms) {
209
+ const normalizedProductTerms = parseBrandTerms(productTerms);
210
+ const normalizedQuery = query.toLowerCase();
211
+ if (normalizedProductTerms.some((term) => normalizedQuery.includes(term))) {
212
+ return true;
213
+ }
214
+ return looksLikeCommercialQuery(query) || tokenizeQuery(query).length >= 3;
215
+ }
216
+ function classifyQuickWinType(position, ctrPercent) {
217
+ if (position <= 8 && ctrPercent <= 3) {
218
+ return "ctr";
219
+ }
220
+ return "position";
221
+ }
222
+ function scoreHighImpressionLowClickOpportunity(input) {
223
+ const impressionScore = Math.min(input.impressions / 100, 60);
224
+ const ctrPenalty = Math.max(0, 15 - input.ctrPercent) * 2;
225
+ const positionBonus = input.position > 0 && input.position <= 20 ? Math.max(0, 20 - input.position) : 0;
226
+ return round(impressionScore + ctrPenalty + positionBonus, 2);
227
+ }
228
+ function scoreQuickWinOpportunity(input) {
229
+ const impressionWeight = Math.min(input.impressions / 150, 50);
230
+ const ctrWeight = Math.max(0, 10 - input.ctrPercent) * 3;
231
+ const positionWeight = input.position > 0 && input.position <= 20 ? Math.max(0, 18 - input.position) * 1.8 : 0;
232
+ return round(impressionWeight + ctrWeight + positionWeight, 2);
233
+ }
234
+ export {
235
+ brandTermsSchemaField,
236
+ buildComparisonMap,
237
+ buildPaginationMetadata,
238
+ buildSearchConsoleQueryBody,
239
+ classifyBrandLabel,
240
+ classifyQuickWinType,
241
+ computeChangePercent,
242
+ currentEndDateSchemaField,
243
+ currentStartDateSchemaField,
244
+ endDateSchemaField,
245
+ inferSearchConsolePropertyType,
246
+ isBrandedQuery,
247
+ looksLikeCommercialQuery,
248
+ looksLikeProductQuery,
249
+ normalizePermissionLevel,
250
+ normalizeSearchConsoleRow,
251
+ normalizeSearchConsoleSiteEntry,
252
+ parseBrandTerms,
253
+ previousEndDateSchemaField,
254
+ previousStartDateSchemaField,
255
+ profileIdSchemaField,
256
+ resolveSearchConsoleSiteUrl,
257
+ round,
258
+ rowLimitSchemaField,
259
+ scoreHighImpressionLowClickOpportunity,
260
+ scoreQuickWinOpportunity,
261
+ searchConsoleAggregationTypes,
262
+ searchConsoleDateRegex,
263
+ searchConsoleDimensionFilterSchema,
264
+ searchConsoleDimensions,
265
+ searchConsoleFilterOperators,
266
+ searchConsoleSearchTypes,
267
+ searchTypeSchemaField,
268
+ siteUrlSchemaField,
269
+ startDateSchemaField,
270
+ startRowSchemaField,
271
+ sumSearchConsoleRows,
272
+ toPercent,
273
+ tokenizeQuery
274
+ };
275
+ //# sourceMappingURL=search-console-utils.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/services/search-console/search-console-utils.ts"],
4
+ "sourcesContent": ["import { z } from \"zod\";\n\nimport type {\n SearchConsoleDimensionFilter,\n SearchConsoleDimensionFilterGroup,\n SearchConsoleSearchAnalyticsRequest,\n SearchConsoleSearchAnalyticsRow,\n SearchConsoleSiteEntry,\n} from \"./search-console-client.js\";\n\nexport const searchConsoleDateRegex = /^\\d{4}-\\d{2}-\\d{2}$/;\n\nexport const searchConsoleSearchTypes = [\"web\", \"image\", \"video\", \"news\", \"discover\"] as const;\nexport const searchConsoleDimensions = [\n \"query\",\n \"page\",\n \"country\",\n \"device\",\n \"date\",\n \"searchAppearance\",\n] as const;\nexport const searchConsoleFilterOperators = [\n \"contains\",\n \"equals\",\n \"notContains\",\n \"notEquals\",\n \"includingRegex\",\n \"excludingRegex\",\n] as const;\nexport const searchConsoleAggregationTypes = [\"auto\", \"byPage\", \"byProperty\"] as const;\n\nexport const searchConsoleDimensionFilterSchema = z.object({\n dimension: z\n .enum(searchConsoleDimensions)\n .describe(\"Dimension to filter on in Search Console (query, page, country, device, date, or searchAppearance)\"),\n operator: z\n .enum(searchConsoleFilterOperators)\n .describe(\"Filter operator supported by Search Console.\"),\n expression: z.string().min(1).describe(\"Filter value or regex expression.\"),\n});\n\nexport const siteUrlSchemaField = z\n .string()\n .optional()\n .describe(\n \"Search Console site URL or domain property identifier (for example https://www.example.com/ or sc-domain:example.com). If omitted, the tool expects the site to be configured elsewhere.\"\n );\n\nexport const profileIdSchemaField = z\n .string()\n .optional()\n .describe(\"Optional business profile identifier for future multi-profile resolution. Defaults to the active profile when supported.\");\n\nexport const startDateSchemaField = z\n .string()\n .regex(searchConsoleDateRegex)\n .describe(\"Start date in YYYY-MM-DD format.\");\n\nexport const endDateSchemaField = z\n .string()\n .regex(searchConsoleDateRegex)\n .describe(\"End date in YYYY-MM-DD format.\");\n\nexport const currentStartDateSchemaField = z\n .string()\n .regex(searchConsoleDateRegex)\n .describe(\"Current comparison period start date in YYYY-MM-DD format.\");\n\nexport const currentEndDateSchemaField = z\n .string()\n .regex(searchConsoleDateRegex)\n .describe(\"Current comparison period end date in YYYY-MM-DD format.\");\n\nexport const previousStartDateSchemaField = z\n .string()\n .regex(searchConsoleDateRegex)\n .describe(\"Previous comparison period start date in YYYY-MM-DD format.\");\n\nexport const previousEndDateSchemaField = z\n .string()\n .regex(searchConsoleDateRegex)\n .describe(\"Previous comparison period end date in YYYY-MM-DD format.\");\n\nexport const rowLimitSchemaField = z\n .number()\n .int()\n .min(1)\n .max(25000)\n .optional()\n .describe(\"Maximum number of rows to return. Search Console supports up to 25,000 rows per request.\");\n\nexport const startRowSchemaField = z\n .number()\n .int()\n .min(0)\n .optional()\n .describe(\"Zero-based row offset for paginated Search Console requests.\");\n\nexport const searchTypeSchemaField = z\n .enum(searchConsoleSearchTypes)\n .optional()\n .describe(\"Search type to query. Defaults to web.\");\n\nexport const brandTermsSchemaField = z\n .array(z.string().min(1))\n .max(50)\n .optional()\n .describe(\"Brand tokens used to separate branded from non-branded queries.\");\n\nexport function resolveSearchConsoleSiteUrl(siteUrl?: string, profileId?: string): string {\n const explicitSiteUrl = siteUrl?.trim();\n if (explicitSiteUrl) {\n return explicitSiteUrl;\n }\n\n if (profileId?.trim()) {\n throw new Error(\n `Missing Search Console site URL. profileId \"${profileId}\" was provided, but business-data based site resolution is not implemented yet. Pass siteUrl explicitly or configure Search Console site resolution first.`\n );\n }\n\n throw new Error(\n \"Missing Search Console site URL. Pass siteUrl explicitly or first call gsc_list_accessible_sites to discover available properties.\"\n );\n}\n\nexport function inferSearchConsolePropertyType(\n siteUrl: string\n): \"domain\" | \"url_prefix\" | \"unknown\" {\n if (siteUrl.startsWith(\"sc-domain:\")) {\n return \"domain\";\n }\n\n if (/^https?:\\/\\//.test(siteUrl)) {\n return \"url_prefix\";\n }\n\n return \"unknown\";\n}\n\nexport function normalizePermissionLevel(permissionLevel?: string): string | undefined {\n return permissionLevel?.trim().toLowerCase() || undefined;\n}\n\nexport function normalizeSearchConsoleSiteEntry(siteEntry: SearchConsoleSiteEntry) {\n return {\n site_url: siteEntry.siteUrl,\n property_type: inferSearchConsolePropertyType(siteEntry.siteUrl),\n permission_level: normalizePermissionLevel(siteEntry.permissionLevel),\n };\n}\n\nexport function toPercent(numerator: number, denominator: number): number {\n if (!Number.isFinite(numerator) || !Number.isFinite(denominator) || denominator <= 0) {\n return 0;\n }\n\n return (numerator / denominator) * 100;\n}\n\nexport function round(value: number, decimals = 2): number {\n if (!Number.isFinite(value)) {\n return 0;\n }\n\n const factor = 10 ** decimals;\n return Math.round(value * factor) / factor;\n}\n\nexport function normalizeSearchConsoleRow(\n row: SearchConsoleSearchAnalyticsRow,\n dimensions: readonly string[] = []\n) {\n const clicks = row.clicks ?? 0;\n const impressions = row.impressions ?? 0;\n const ctr = typeof row.ctr === \"number\" ? row.ctr * 100 : toPercent(clicks, impressions);\n const keys = row.keys ?? [];\n\n return {\n keys,\n dimensions: dimensions.reduce<Record<string, string>>((accumulator, dimension, index) => {\n accumulator[dimension] = keys[index] ?? \"\";\n return accumulator;\n }, {}),\n clicks: round(clicks, 2),\n impressions: round(impressions, 2),\n ctr_percent: round(ctr, 2),\n position: round(row.position ?? 0, 2),\n };\n}\n\nexport function buildSearchConsoleQueryBody(input: {\n startDate: string;\n endDate: string;\n dimensions?: string[];\n searchType?: string;\n dimensionFilters?: SearchConsoleDimensionFilter[];\n aggregationType?: string;\n rowLimit?: number;\n startRow?: number;\n}): SearchConsoleSearchAnalyticsRequest {\n const dimensionFilterGroups: SearchConsoleDimensionFilterGroup[] | undefined =\n input.dimensionFilters && input.dimensionFilters.length > 0\n ? [\n {\n groupType: \"and\",\n filters: input.dimensionFilters,\n },\n ]\n : undefined;\n\n return {\n startDate: input.startDate,\n endDate: input.endDate,\n dimensions: input.dimensions,\n type: input.searchType ?? \"web\",\n dimensionFilterGroups,\n aggregationType: input.aggregationType,\n rowLimit: input.rowLimit,\n startRow: input.startRow,\n };\n}\n\nexport function buildPaginationMetadata(input: {\n startRow?: number;\n rowLimit?: number;\n returnedRows: number;\n}) {\n const startRow = input.startRow ?? 0;\n const rowLimit = input.rowLimit;\n const isPartial = Boolean(rowLimit) && input.returnedRows === rowLimit;\n const nextStartRow = isPartial ? startRow + input.returnedRows : undefined;\n\n return {\n is_partial: isPartial,\n start_row: startRow,\n row_limit: rowLimit,\n returned_rows: input.returnedRows,\n next_start_row: nextStartRow,\n continuation_instructions: nextStartRow\n ? \"This Search Console response may be truncated at row_limit. If you need additional rows, call the same tool again with startRow set to next_start_row.\"\n : undefined,\n };\n}\n\nexport function parseBrandTerms(brandTerms?: string[]): string[] {\n return (brandTerms ?? []).map((term) => term.trim().toLowerCase()).filter(Boolean);\n}\n\nexport function isBrandedQuery(query: string, brandTerms?: string[]): boolean {\n const normalizedQuery = query.trim().toLowerCase();\n const normalizedBrandTerms = parseBrandTerms(brandTerms);\n\n if (!normalizedQuery || normalizedBrandTerms.length === 0) {\n return false;\n }\n\n return normalizedBrandTerms.some((brandTerm) => normalizedQuery.includes(brandTerm));\n}\n\nexport function classifyBrandLabel(query: string, brandTerms?: string[]): \"brand\" | \"non_brand\" {\n return isBrandedQuery(query, brandTerms) ? \"brand\" : \"non_brand\";\n}\n\nexport function computeChangePercent(currentValue: number, previousValue: number): number | null {\n if (!Number.isFinite(currentValue) || !Number.isFinite(previousValue)) {\n return null;\n }\n\n if (previousValue === 0) {\n if (currentValue === 0) {\n return 0;\n }\n\n return null;\n }\n\n return round(((currentValue - previousValue) / previousValue) * 100, 2);\n}\n\nexport function sumSearchConsoleRows(\n rows: Array<ReturnType<typeof normalizeSearchConsoleRow>>\n): {\n clicks: number;\n impressions: number;\n ctrPercent: number;\n averagePosition: number;\n} {\n const totals = rows.reduce(\n (accumulator, row) => {\n accumulator.clicks += row.clicks;\n accumulator.impressions += row.impressions;\n accumulator.weightedPosition += row.position * row.impressions;\n return accumulator;\n },\n {\n clicks: 0,\n impressions: 0,\n weightedPosition: 0,\n }\n );\n\n return {\n clicks: round(totals.clicks, 2),\n impressions: round(totals.impressions, 2),\n ctrPercent: round(toPercent(totals.clicks, totals.impressions), 2),\n averagePosition: totals.impressions > 0 ? round(totals.weightedPosition / totals.impressions, 2) : 0,\n };\n}\n\nexport function buildComparisonMap(\n rows: Array<ReturnType<typeof normalizeSearchConsoleRow>>,\n keyBuilder: (row: ReturnType<typeof normalizeSearchConsoleRow>) => string\n) {\n return new Map(rows.map((row) => [keyBuilder(row), row]));\n}\n\nexport function tokenizeQuery(query: string): string[] {\n return query\n .toLowerCase()\n .split(/[^a-z0-9\u00E1\u00E9\u00ED\u00F3\u00FA\u00FC\u00F1]+/i)\n .map((token) => token.trim())\n .filter(Boolean);\n}\n\nexport function looksLikeCommercialQuery(query: string): boolean {\n const tokens = tokenizeQuery(query);\n const commercialTerms = new Set([\n \"comprar\",\n \"precio\",\n \"precios\",\n \"cuanto\",\n \"oferta\",\n \"ofertas\",\n \"envio\",\n \"shop\",\n \"tienda\",\n \"modelo\",\n \"medida\",\n \"ml\",\n \"cm\",\n \"kg\",\n ]);\n\n return tokens.some((token) => commercialTerms.has(token));\n}\n\nexport function looksLikeProductQuery(query: string, productTerms?: string[]): boolean {\n const normalizedProductTerms = parseBrandTerms(productTerms);\n const normalizedQuery = query.toLowerCase();\n\n if (normalizedProductTerms.some((term) => normalizedQuery.includes(term))) {\n return true;\n }\n\n return looksLikeCommercialQuery(query) || tokenizeQuery(query).length >= 3;\n}\n\nexport function classifyQuickWinType(position: number, ctrPercent: number): \"ctr\" | \"position\" {\n if (position <= 8 && ctrPercent <= 3) {\n return \"ctr\";\n }\n\n return \"position\";\n}\n\nexport function scoreHighImpressionLowClickOpportunity(input: {\n impressions: number;\n ctrPercent: number;\n position: number;\n}): number {\n const impressionScore = Math.min(input.impressions / 100, 60);\n const ctrPenalty = Math.max(0, 15 - input.ctrPercent) * 2;\n const positionBonus = input.position > 0 && input.position <= 20 ? Math.max(0, 20 - input.position) : 0;\n\n return round(impressionScore + ctrPenalty + positionBonus, 2);\n}\n\nexport function scoreQuickWinOpportunity(input: {\n impressions: number;\n ctrPercent: number;\n position: number;\n}): number {\n const impressionWeight = Math.min(input.impressions / 150, 50);\n const ctrWeight = Math.max(0, 10 - input.ctrPercent) * 3;\n const positionWeight = input.position > 0 && input.position <= 20 ? Math.max(0, 18 - input.position) * 1.8 : 0;\n\n return round(impressionWeight + ctrWeight + positionWeight, 2);\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -4,7 +4,7 @@ import { listProfiles } from "../../config/profile-store.js";
4
4
  const listProfilesSchema = z.object({});
5
5
  async function listProfilesHandler() {
6
6
  try {
7
- const profiles = await listProfiles();
7
+ const profiles = await listProfiles({ activeOnly: true });
8
8
  return object({
9
9
  total_profiles: profiles.length,
10
10
  profiles: profiles.map((profile) => ({
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/tools/config/list-profiles.ts"],
4
- "sourcesContent": ["import { error, object } from \"mcp-use/server\";\nimport { z } from \"zod\";\n\nimport { listProfiles } from \"../../config/profile-store.js\";\n\nexport const listProfilesSchema = z.object({});\n\nexport async function listProfilesHandler() {\n try {\n const profiles = await listProfiles();\n\n return object({\n total_profiles: profiles.length,\n profiles: profiles.map((profile) => ({\n profile_id: profile.id,\n name: profile.name,\n is_active: profile.isActive,\n created_at: profile.createdAt.toISOString(),\n updated_at: profile.updatedAt.toISOString(),\n })),\n });\n } catch (err) {\n return error(err instanceof Error ? err.message : \"Failed to list profiles\");\n }\n}\n"],
5
- "mappings": "AAAA,SAAS,OAAO,cAAc;AAC9B,SAAS,SAAS;AAElB,SAAS,oBAAoB;AAEtB,MAAM,qBAAqB,EAAE,OAAO,CAAC,CAAC;AAE7C,eAAsB,sBAAsB;AAC1C,MAAI;AACF,UAAM,WAAW,MAAM,aAAa;AAEpC,WAAO,OAAO;AAAA,MACZ,gBAAgB,SAAS;AAAA,MACzB,UAAU,SAAS,IAAI,CAAC,aAAa;AAAA,QACnC,YAAY,QAAQ;AAAA,QACpB,MAAM,QAAQ;AAAA,QACd,WAAW,QAAQ;AAAA,QACnB,YAAY,QAAQ,UAAU,YAAY;AAAA,QAC1C,YAAY,QAAQ,UAAU,YAAY;AAAA,MAC5C,EAAE;AAAA,IACJ,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,WAAO,MAAM,eAAe,QAAQ,IAAI,UAAU,yBAAyB;AAAA,EAC7E;AACF;",
4
+ "sourcesContent": ["import { error, object } from \"mcp-use/server\";\nimport { z } from \"zod\";\n\nimport { listProfiles } from \"../../config/profile-store.js\";\n\nexport const listProfilesSchema = z.object({});\n\nexport async function listProfilesHandler() {\n try {\n const profiles = await listProfiles({ activeOnly: true });\n\n return object({\n total_profiles: profiles.length,\n profiles: profiles.map((profile) => ({\n profile_id: profile.id,\n name: profile.name,\n is_active: profile.isActive,\n created_at: profile.createdAt.toISOString(),\n updated_at: profile.updatedAt.toISOString(),\n })),\n });\n } catch (err) {\n return error(err instanceof Error ? err.message : \"Failed to list profiles\");\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,OAAO,cAAc;AAC9B,SAAS,SAAS;AAElB,SAAS,oBAAoB;AAEtB,MAAM,qBAAqB,EAAE,OAAO,CAAC,CAAC;AAE7C,eAAsB,sBAAsB;AAC1C,MAAI;AACF,UAAM,WAAW,MAAM,aAAa,EAAE,YAAY,KAAK,CAAC;AAExD,WAAO,OAAO;AAAA,MACZ,gBAAgB,SAAS;AAAA,MACzB,UAAU,SAAS,IAAI,CAAC,aAAa;AAAA,QACnC,YAAY,QAAQ;AAAA,QACpB,MAAM,QAAQ;AAAA,QACd,WAAW,QAAQ;AAAA,QACnB,YAAY,QAAQ,UAAU,YAAY;AAAA,QAC1C,YAAY,QAAQ,UAAU,YAAY;AAAA,MAC5C,EAAE;AAAA,IACJ,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,WAAO,MAAM,eAAe,QAAQ,IAAI,UAAU,yBAAyB;AAAA,EAC7E;AACF;",
6
6
  "names": []
7
7
  }
@@ -13,7 +13,7 @@ import {
13
13
  toPercent,
14
14
  normalizeSearchConsoleRow,
15
15
  round
16
- } from "../../search-console/search-console-utils.js";
16
+ } from "../../services/search-console/search-console-utils.js";
17
17
  import { stripNulls } from "../../utils/strip-payload.js";
18
18
  const searchConsoleCountryBreakdownSchema = z.object({
19
19
  startDate: startDateSchemaField,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/tools/search-console/country-breakdown.ts"],
4
- "sourcesContent": ["import { error, object } from \"mcp-use/server\";\nimport { z } from \"zod\";\n\nimport { querySearchConsolePerformance } from \"../../services/search-console/search-console-client.js\";\nimport {\n buildSearchConsoleQueryBody,\n endDateSchemaField,\n profileIdSchemaField,\n resolveSearchConsoleSiteUrl,\n searchTypeSchemaField,\n siteUrlSchemaField,\n startDateSchemaField,\n sumSearchConsoleRows,\n toPercent,\n normalizeSearchConsoleRow,\n round,\n} from \"../../search-console/search-console-utils.js\";\nimport { stripNulls } from \"../../utils/strip-payload.js\";\n\nexport const searchConsoleCountryBreakdownSchema = z.object({\n startDate: startDateSchemaField,\n endDate: endDateSchemaField,\n siteUrl: siteUrlSchemaField,\n profileId: profileIdSchemaField,\n searchType: searchTypeSchemaField,\n queryContains: z.string().optional().describe(\"Optional query substring filter.\"),\n pageContains: z.string().optional().describe(\"Optional page substring filter.\"),\n});\n\nexport async function searchConsoleCountryBreakdownHandler(\n params: z.infer<typeof searchConsoleCountryBreakdownSchema>\n) {\n try {\n const siteUrl = resolveSearchConsoleSiteUrl(params.siteUrl, params.profileId);\n const dimensionFilters = [];\n\n if (params.queryContains?.trim()) {\n dimensionFilters.push({\n dimension: \"query\",\n operator: \"contains\",\n expression: params.queryContains.trim(),\n });\n }\n\n if (params.pageContains?.trim()) {\n dimensionFilters.push({\n dimension: \"page\",\n operator: \"contains\",\n expression: params.pageContains.trim(),\n });\n }\n\n const response = await querySearchConsolePerformance(\n siteUrl,\n buildSearchConsoleQueryBody({\n startDate: params.startDate,\n endDate: params.endDate,\n dimensions: [\"country\"],\n searchType: params.searchType,\n dimensionFilters,\n })\n );\n\n const rows = (response.rows ?? []).map((row) => normalizeSearchConsoleRow(row, [\"country\"]));\n const totals = sumSearchConsoleRows(rows);\n\n return object(\n stripNulls({\n site_url: siteUrl,\n date_range: {\n start_date: params.startDate,\n end_date: params.endDate,\n },\n country_breakdown: rows.map((row) => ({\n country: row.dimensions.country,\n clicks: row.clicks,\n impressions: row.impressions,\n ctr_percent: row.ctr_percent,\n position: row.position,\n impression_share_percent: round(toPercent(row.impressions, totals.impressions), 2),\n click_share_percent: round(toPercent(row.clicks, totals.clicks), 2),\n })),\n })\n );\n } catch (err) {\n return error(\n err instanceof Error ? err.message : \"Failed to fetch Search Console country breakdown\"\n );\n }\n}\n"],
4
+ "sourcesContent": ["import { error, object } from \"mcp-use/server\";\nimport { z } from \"zod\";\n\nimport { querySearchConsolePerformance } from \"../../services/search-console/search-console-client.js\";\nimport {\n buildSearchConsoleQueryBody,\n endDateSchemaField,\n profileIdSchemaField,\n resolveSearchConsoleSiteUrl,\n searchTypeSchemaField,\n siteUrlSchemaField,\n startDateSchemaField,\n sumSearchConsoleRows,\n toPercent,\n normalizeSearchConsoleRow,\n round,\n} from \"../../services/search-console/search-console-utils.js\";\nimport { stripNulls } from \"../../utils/strip-payload.js\";\n\nexport const searchConsoleCountryBreakdownSchema = z.object({\n startDate: startDateSchemaField,\n endDate: endDateSchemaField,\n siteUrl: siteUrlSchemaField,\n profileId: profileIdSchemaField,\n searchType: searchTypeSchemaField,\n queryContains: z.string().optional().describe(\"Optional query substring filter.\"),\n pageContains: z.string().optional().describe(\"Optional page substring filter.\"),\n});\n\nexport async function searchConsoleCountryBreakdownHandler(\n params: z.infer<typeof searchConsoleCountryBreakdownSchema>\n) {\n try {\n const siteUrl = resolveSearchConsoleSiteUrl(params.siteUrl, params.profileId);\n const dimensionFilters = [];\n\n if (params.queryContains?.trim()) {\n dimensionFilters.push({\n dimension: \"query\",\n operator: \"contains\",\n expression: params.queryContains.trim(),\n });\n }\n\n if (params.pageContains?.trim()) {\n dimensionFilters.push({\n dimension: \"page\",\n operator: \"contains\",\n expression: params.pageContains.trim(),\n });\n }\n\n const response = await querySearchConsolePerformance(\n siteUrl,\n buildSearchConsoleQueryBody({\n startDate: params.startDate,\n endDate: params.endDate,\n dimensions: [\"country\"],\n searchType: params.searchType,\n dimensionFilters,\n })\n );\n\n const rows = (response.rows ?? []).map((row) => normalizeSearchConsoleRow(row, [\"country\"]));\n const totals = sumSearchConsoleRows(rows);\n\n return object(\n stripNulls({\n site_url: siteUrl,\n date_range: {\n start_date: params.startDate,\n end_date: params.endDate,\n },\n country_breakdown: rows.map((row) => ({\n country: row.dimensions.country,\n clicks: row.clicks,\n impressions: row.impressions,\n ctr_percent: row.ctr_percent,\n position: row.position,\n impression_share_percent: round(toPercent(row.impressions, totals.impressions), 2),\n click_share_percent: round(toPercent(row.clicks, totals.clicks), 2),\n })),\n })\n );\n } catch (err) {\n return error(\n err instanceof Error ? err.message : \"Failed to fetch Search Console country breakdown\"\n );\n }\n}\n"],
5
5
  "mappings": "AAAA,SAAS,OAAO,cAAc;AAC9B,SAAS,SAAS;AAElB,SAAS,qCAAqC;AAC9C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,kBAAkB;AAEpB,MAAM,sCAAsC,EAAE,OAAO;AAAA,EAC1D,WAAW;AAAA,EACX,SAAS;AAAA,EACT,SAAS;AAAA,EACT,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,eAAe,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,kCAAkC;AAAA,EAChF,cAAc,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,iCAAiC;AAChF,CAAC;AAED,eAAsB,qCACpB,QACA;AACA,MAAI;AACF,UAAM,UAAU,4BAA4B,OAAO,SAAS,OAAO,SAAS;AAC5E,UAAM,mBAAmB,CAAC;AAE1B,QAAI,OAAO,eAAe,KAAK,GAAG;AAChC,uBAAiB,KAAK;AAAA,QACpB,WAAW;AAAA,QACX,UAAU;AAAA,QACV,YAAY,OAAO,cAAc,KAAK;AAAA,MACxC,CAAC;AAAA,IACH;AAEA,QAAI,OAAO,cAAc,KAAK,GAAG;AAC/B,uBAAiB,KAAK;AAAA,QACpB,WAAW;AAAA,QACX,UAAU;AAAA,QACV,YAAY,OAAO,aAAa,KAAK;AAAA,MACvC,CAAC;AAAA,IACH;AAEA,UAAM,WAAW,MAAM;AAAA,MACrB;AAAA,MACA,4BAA4B;AAAA,QAC1B,WAAW,OAAO;AAAA,QAClB,SAAS,OAAO;AAAA,QAChB,YAAY,CAAC,SAAS;AAAA,QACtB,YAAY,OAAO;AAAA,QACnB;AAAA,MACF,CAAC;AAAA,IACH;AAEA,UAAM,QAAQ,SAAS,QAAQ,CAAC,GAAG,IAAI,CAAC,QAAQ,0BAA0B,KAAK,CAAC,SAAS,CAAC,CAAC;AAC3F,UAAM,SAAS,qBAAqB,IAAI;AAExC,WAAO;AAAA,MACL,WAAW;AAAA,QACT,UAAU;AAAA,QACV,YAAY;AAAA,UACV,YAAY,OAAO;AAAA,UACnB,UAAU,OAAO;AAAA,QACnB;AAAA,QACA,mBAAmB,KAAK,IAAI,CAAC,SAAS;AAAA,UACpC,SAAS,IAAI,WAAW;AAAA,UACxB,QAAQ,IAAI;AAAA,UACZ,aAAa,IAAI;AAAA,UACjB,aAAa,IAAI;AAAA,UACjB,UAAU,IAAI;AAAA,UACd,0BAA0B,MAAM,UAAU,IAAI,aAAa,OAAO,WAAW,GAAG,CAAC;AAAA,UACjF,qBAAqB,MAAM,UAAU,IAAI,QAAQ,OAAO,MAAM,GAAG,CAAC;AAAA,QACpE,EAAE;AAAA,MACJ,CAAC;AAAA,IACH;AAAA,EACF,SAAS,KAAK;AACZ,WAAO;AAAA,MACL,eAAe,QAAQ,IAAI,UAAU;AAAA,IACvC;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -13,7 +13,7 @@ import {
13
13
  toPercent,
14
14
  normalizeSearchConsoleRow,
15
15
  round
16
- } from "../../search-console/search-console-utils.js";
16
+ } from "../../services/search-console/search-console-utils.js";
17
17
  import { stripNulls } from "../../utils/strip-payload.js";
18
18
  const searchConsoleDeviceBreakdownSchema = z.object({
19
19
  startDate: startDateSchemaField,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/tools/search-console/device-breakdown.ts"],
4
- "sourcesContent": ["import { error, object } from \"mcp-use/server\";\nimport { z } from \"zod\";\n\nimport { querySearchConsolePerformance } from \"../../services/search-console/search-console-client.js\";\nimport {\n buildSearchConsoleQueryBody,\n endDateSchemaField,\n profileIdSchemaField,\n resolveSearchConsoleSiteUrl,\n searchTypeSchemaField,\n siteUrlSchemaField,\n startDateSchemaField,\n sumSearchConsoleRows,\n toPercent,\n normalizeSearchConsoleRow,\n round,\n} from \"../../search-console/search-console-utils.js\";\nimport { stripNulls } from \"../../utils/strip-payload.js\";\n\nexport const searchConsoleDeviceBreakdownSchema = z.object({\n startDate: startDateSchemaField,\n endDate: endDateSchemaField,\n siteUrl: siteUrlSchemaField,\n profileId: profileIdSchemaField,\n searchType: searchTypeSchemaField,\n queryContains: z.string().optional().describe(\"Optional query substring filter.\"),\n pageContains: z.string().optional().describe(\"Optional page substring filter.\"),\n});\n\nexport async function searchConsoleDeviceBreakdownHandler(\n params: z.infer<typeof searchConsoleDeviceBreakdownSchema>\n) {\n try {\n const siteUrl = resolveSearchConsoleSiteUrl(params.siteUrl, params.profileId);\n const dimensionFilters = [];\n\n if (params.queryContains?.trim()) {\n dimensionFilters.push({\n dimension: \"query\",\n operator: \"contains\",\n expression: params.queryContains.trim(),\n });\n }\n\n if (params.pageContains?.trim()) {\n dimensionFilters.push({\n dimension: \"page\",\n operator: \"contains\",\n expression: params.pageContains.trim(),\n });\n }\n\n const response = await querySearchConsolePerformance(\n siteUrl,\n buildSearchConsoleQueryBody({\n startDate: params.startDate,\n endDate: params.endDate,\n dimensions: [\"device\"],\n searchType: params.searchType,\n dimensionFilters,\n })\n );\n\n const rows = (response.rows ?? []).map((row) => normalizeSearchConsoleRow(row, [\"device\"]));\n const totals = sumSearchConsoleRows(rows);\n\n return object(\n stripNulls({\n site_url: siteUrl,\n date_range: {\n start_date: params.startDate,\n end_date: params.endDate,\n },\n device_breakdown: rows.map((row) => ({\n device: row.dimensions.device,\n clicks: row.clicks,\n impressions: row.impressions,\n ctr_percent: row.ctr_percent,\n position: row.position,\n impression_share_percent: round(toPercent(row.impressions, totals.impressions), 2),\n click_share_percent: round(toPercent(row.clicks, totals.clicks), 2),\n })),\n })\n );\n } catch (err) {\n return error(\n err instanceof Error ? err.message : \"Failed to fetch Search Console device breakdown\"\n );\n }\n}\n"],
4
+ "sourcesContent": ["import { error, object } from \"mcp-use/server\";\nimport { z } from \"zod\";\n\nimport { querySearchConsolePerformance } from \"../../services/search-console/search-console-client.js\";\nimport {\n buildSearchConsoleQueryBody,\n endDateSchemaField,\n profileIdSchemaField,\n resolveSearchConsoleSiteUrl,\n searchTypeSchemaField,\n siteUrlSchemaField,\n startDateSchemaField,\n sumSearchConsoleRows,\n toPercent,\n normalizeSearchConsoleRow,\n round,\n} from \"../../services/search-console/search-console-utils.js\";\nimport { stripNulls } from \"../../utils/strip-payload.js\";\n\nexport const searchConsoleDeviceBreakdownSchema = z.object({\n startDate: startDateSchemaField,\n endDate: endDateSchemaField,\n siteUrl: siteUrlSchemaField,\n profileId: profileIdSchemaField,\n searchType: searchTypeSchemaField,\n queryContains: z.string().optional().describe(\"Optional query substring filter.\"),\n pageContains: z.string().optional().describe(\"Optional page substring filter.\"),\n});\n\nexport async function searchConsoleDeviceBreakdownHandler(\n params: z.infer<typeof searchConsoleDeviceBreakdownSchema>\n) {\n try {\n const siteUrl = resolveSearchConsoleSiteUrl(params.siteUrl, params.profileId);\n const dimensionFilters = [];\n\n if (params.queryContains?.trim()) {\n dimensionFilters.push({\n dimension: \"query\",\n operator: \"contains\",\n expression: params.queryContains.trim(),\n });\n }\n\n if (params.pageContains?.trim()) {\n dimensionFilters.push({\n dimension: \"page\",\n operator: \"contains\",\n expression: params.pageContains.trim(),\n });\n }\n\n const response = await querySearchConsolePerformance(\n siteUrl,\n buildSearchConsoleQueryBody({\n startDate: params.startDate,\n endDate: params.endDate,\n dimensions: [\"device\"],\n searchType: params.searchType,\n dimensionFilters,\n })\n );\n\n const rows = (response.rows ?? []).map((row) => normalizeSearchConsoleRow(row, [\"device\"]));\n const totals = sumSearchConsoleRows(rows);\n\n return object(\n stripNulls({\n site_url: siteUrl,\n date_range: {\n start_date: params.startDate,\n end_date: params.endDate,\n },\n device_breakdown: rows.map((row) => ({\n device: row.dimensions.device,\n clicks: row.clicks,\n impressions: row.impressions,\n ctr_percent: row.ctr_percent,\n position: row.position,\n impression_share_percent: round(toPercent(row.impressions, totals.impressions), 2),\n click_share_percent: round(toPercent(row.clicks, totals.clicks), 2),\n })),\n })\n );\n } catch (err) {\n return error(\n err instanceof Error ? err.message : \"Failed to fetch Search Console device breakdown\"\n );\n }\n}\n"],
5
5
  "mappings": "AAAA,SAAS,OAAO,cAAc;AAC9B,SAAS,SAAS;AAElB,SAAS,qCAAqC;AAC9C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,kBAAkB;AAEpB,MAAM,qCAAqC,EAAE,OAAO;AAAA,EACzD,WAAW;AAAA,EACX,SAAS;AAAA,EACT,SAAS;AAAA,EACT,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,eAAe,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,kCAAkC;AAAA,EAChF,cAAc,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,iCAAiC;AAChF,CAAC;AAED,eAAsB,oCACpB,QACA;AACA,MAAI;AACF,UAAM,UAAU,4BAA4B,OAAO,SAAS,OAAO,SAAS;AAC5E,UAAM,mBAAmB,CAAC;AAE1B,QAAI,OAAO,eAAe,KAAK,GAAG;AAChC,uBAAiB,KAAK;AAAA,QACpB,WAAW;AAAA,QACX,UAAU;AAAA,QACV,YAAY,OAAO,cAAc,KAAK;AAAA,MACxC,CAAC;AAAA,IACH;AAEA,QAAI,OAAO,cAAc,KAAK,GAAG;AAC/B,uBAAiB,KAAK;AAAA,QACpB,WAAW;AAAA,QACX,UAAU;AAAA,QACV,YAAY,OAAO,aAAa,KAAK;AAAA,MACvC,CAAC;AAAA,IACH;AAEA,UAAM,WAAW,MAAM;AAAA,MACrB;AAAA,MACA,4BAA4B;AAAA,QAC1B,WAAW,OAAO;AAAA,QAClB,SAAS,OAAO;AAAA,QAChB,YAAY,CAAC,QAAQ;AAAA,QACrB,YAAY,OAAO;AAAA,QACnB;AAAA,MACF,CAAC;AAAA,IACH;AAEA,UAAM,QAAQ,SAAS,QAAQ,CAAC,GAAG,IAAI,CAAC,QAAQ,0BAA0B,KAAK,CAAC,QAAQ,CAAC,CAAC;AAC1F,UAAM,SAAS,qBAAqB,IAAI;AAExC,WAAO;AAAA,MACL,WAAW;AAAA,QACT,UAAU;AAAA,QACV,YAAY;AAAA,UACV,YAAY,OAAO;AAAA,UACnB,UAAU,OAAO;AAAA,QACnB;AAAA,QACA,kBAAkB,KAAK,IAAI,CAAC,SAAS;AAAA,UACnC,QAAQ,IAAI,WAAW;AAAA,UACvB,QAAQ,IAAI;AAAA,UACZ,aAAa,IAAI;AAAA,UACjB,aAAa,IAAI;AAAA,UACjB,UAAU,IAAI;AAAA,UACd,0BAA0B,MAAM,UAAU,IAAI,aAAa,OAAO,WAAW,GAAG,CAAC;AAAA,UACjF,qBAAqB,MAAM,UAAU,IAAI,QAAQ,OAAO,MAAM,GAAG,CAAC;AAAA,QACpE,EAAE;AAAA,MACJ,CAAC;AAAA,IACH;AAAA,EACF,SAAS,KAAK;AACZ,WAAO;AAAA,MACL,eAAe,QAAQ,IAAI,UAAU;AAAA,IACvC;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -14,7 +14,7 @@ import {
14
14
  siteUrlSchemaField,
15
15
  startDateSchemaField,
16
16
  normalizeSearchConsoleRow
17
- } from "../../search-console/search-console-utils.js";
17
+ } from "../../services/search-console/search-console-utils.js";
18
18
  import { stripNulls } from "../../utils/strip-payload.js";
19
19
  const searchConsoleHighImpressionLowClickQueriesSchema = z.object({
20
20
  startDate: startDateSchemaField,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/tools/search-console/high-impression-low-click-queries.ts"],
4
- "sourcesContent": ["import { error, object } from \"mcp-use/server\";\nimport { z } from \"zod\";\n\nimport { querySearchConsolePerformance } from \"../../services/search-console/search-console-client.js\";\nimport {\n brandTermsSchemaField,\n buildSearchConsoleQueryBody,\n classifyBrandLabel,\n endDateSchemaField,\n profileIdSchemaField,\n resolveSearchConsoleSiteUrl,\n rowLimitSchemaField,\n scoreHighImpressionLowClickOpportunity,\n searchTypeSchemaField,\n siteUrlSchemaField,\n startDateSchemaField,\n normalizeSearchConsoleRow,\n} from \"../../search-console/search-console-utils.js\";\nimport { stripNulls } from \"../../utils/strip-payload.js\";\n\nexport const searchConsoleHighImpressionLowClickQueriesSchema = z.object({\n startDate: startDateSchemaField,\n endDate: endDateSchemaField,\n siteUrl: siteUrlSchemaField,\n profileId: profileIdSchemaField,\n searchType: searchTypeSchemaField,\n brandTerms: brandTermsSchemaField,\n includeBrand: z\n .enum([\"all\", \"only_brand\", \"only_non_brand\"])\n .optional()\n .describe(\"How to filter the query list when brandTerms are provided.\"),\n minImpressions: z\n .number()\n .min(1)\n .optional()\n .describe(\"Minimum impressions required to consider a query an opportunity.\"),\n maxCtr: z\n .number()\n .min(0)\n .max(100)\n .optional()\n .describe(\"Maximum CTR percentage allowed to count as low capture.\"),\n rowLimit: rowLimitSchemaField,\n});\n\nexport async function searchConsoleHighImpressionLowClickQueriesHandler(\n params: z.infer<typeof searchConsoleHighImpressionLowClickQueriesSchema>\n) {\n try {\n const siteUrl = resolveSearchConsoleSiteUrl(params.siteUrl, params.profileId);\n const minImpressions = params.minImpressions ?? 100;\n const maxCtr = params.maxCtr ?? 3;\n const includeBrand = params.includeBrand ?? \"all\";\n const response = await querySearchConsolePerformance(\n siteUrl,\n buildSearchConsoleQueryBody({\n startDate: params.startDate,\n endDate: params.endDate,\n dimensions: [\"query\"],\n searchType: params.searchType,\n rowLimit: params.rowLimit ?? 250,\n })\n );\n\n const opportunities = (response.rows ?? [])\n .map((row) => normalizeSearchConsoleRow(row, [\"query\"]))\n .map((row) => ({\n query: row.dimensions.query,\n clicks: row.clicks,\n impressions: row.impressions,\n ctr_percent: row.ctr_percent,\n position: row.position,\n brand_classification: classifyBrandLabel(row.dimensions.query, params.brandTerms),\n }))\n .filter((row) => row.impressions >= minImpressions && row.ctr_percent <= maxCtr)\n .filter((row) => {\n if (includeBrand === \"only_brand\") {\n return row.brand_classification === \"brand\";\n }\n\n if (includeBrand === \"only_non_brand\") {\n return row.brand_classification === \"non_brand\";\n }\n\n return true;\n })\n .map((row) => ({\n ...row,\n opportunity_score: scoreHighImpressionLowClickOpportunity({\n impressions: row.impressions,\n ctrPercent: row.ctr_percent,\n position: row.position,\n }),\n reason: \"High impression volume with low click capture.\",\n }))\n .sort((left, right) => right.opportunity_score - left.opportunity_score);\n\n return object(\n stripNulls({\n site_url: siteUrl,\n date_range: {\n start_date: params.startDate,\n end_date: params.endDate,\n },\n thresholds: {\n min_impressions: minImpressions,\n max_ctr_percent: maxCtr,\n },\n opportunities,\n })\n );\n } catch (err) {\n return error(\n err instanceof Error\n ? err.message\n : \"Failed to find Search Console high-impression low-click queries\"\n );\n }\n}\n"],
4
+ "sourcesContent": ["import { error, object } from \"mcp-use/server\";\nimport { z } from \"zod\";\n\nimport { querySearchConsolePerformance } from \"../../services/search-console/search-console-client.js\";\nimport {\n brandTermsSchemaField,\n buildSearchConsoleQueryBody,\n classifyBrandLabel,\n endDateSchemaField,\n profileIdSchemaField,\n resolveSearchConsoleSiteUrl,\n rowLimitSchemaField,\n scoreHighImpressionLowClickOpportunity,\n searchTypeSchemaField,\n siteUrlSchemaField,\n startDateSchemaField,\n normalizeSearchConsoleRow,\n} from \"../../services/search-console/search-console-utils.js\";\nimport { stripNulls } from \"../../utils/strip-payload.js\";\n\nexport const searchConsoleHighImpressionLowClickQueriesSchema = z.object({\n startDate: startDateSchemaField,\n endDate: endDateSchemaField,\n siteUrl: siteUrlSchemaField,\n profileId: profileIdSchemaField,\n searchType: searchTypeSchemaField,\n brandTerms: brandTermsSchemaField,\n includeBrand: z\n .enum([\"all\", \"only_brand\", \"only_non_brand\"])\n .optional()\n .describe(\"How to filter the query list when brandTerms are provided.\"),\n minImpressions: z\n .number()\n .min(1)\n .optional()\n .describe(\"Minimum impressions required to consider a query an opportunity.\"),\n maxCtr: z\n .number()\n .min(0)\n .max(100)\n .optional()\n .describe(\"Maximum CTR percentage allowed to count as low capture.\"),\n rowLimit: rowLimitSchemaField,\n});\n\nexport async function searchConsoleHighImpressionLowClickQueriesHandler(\n params: z.infer<typeof searchConsoleHighImpressionLowClickQueriesSchema>\n) {\n try {\n const siteUrl = resolveSearchConsoleSiteUrl(params.siteUrl, params.profileId);\n const minImpressions = params.minImpressions ?? 100;\n const maxCtr = params.maxCtr ?? 3;\n const includeBrand = params.includeBrand ?? \"all\";\n const response = await querySearchConsolePerformance(\n siteUrl,\n buildSearchConsoleQueryBody({\n startDate: params.startDate,\n endDate: params.endDate,\n dimensions: [\"query\"],\n searchType: params.searchType,\n rowLimit: params.rowLimit ?? 250,\n })\n );\n\n const opportunities = (response.rows ?? [])\n .map((row) => normalizeSearchConsoleRow(row, [\"query\"]))\n .map((row) => ({\n query: row.dimensions.query,\n clicks: row.clicks,\n impressions: row.impressions,\n ctr_percent: row.ctr_percent,\n position: row.position,\n brand_classification: classifyBrandLabel(row.dimensions.query, params.brandTerms),\n }))\n .filter((row) => row.impressions >= minImpressions && row.ctr_percent <= maxCtr)\n .filter((row) => {\n if (includeBrand === \"only_brand\") {\n return row.brand_classification === \"brand\";\n }\n\n if (includeBrand === \"only_non_brand\") {\n return row.brand_classification === \"non_brand\";\n }\n\n return true;\n })\n .map((row) => ({\n ...row,\n opportunity_score: scoreHighImpressionLowClickOpportunity({\n impressions: row.impressions,\n ctrPercent: row.ctr_percent,\n position: row.position,\n }),\n reason: \"High impression volume with low click capture.\",\n }))\n .sort((left, right) => right.opportunity_score - left.opportunity_score);\n\n return object(\n stripNulls({\n site_url: siteUrl,\n date_range: {\n start_date: params.startDate,\n end_date: params.endDate,\n },\n thresholds: {\n min_impressions: minImpressions,\n max_ctr_percent: maxCtr,\n },\n opportunities,\n })\n );\n } catch (err) {\n return error(\n err instanceof Error\n ? err.message\n : \"Failed to find Search Console high-impression low-click queries\"\n );\n }\n}\n"],
5
5
  "mappings": "AAAA,SAAS,OAAO,cAAc;AAC9B,SAAS,SAAS;AAElB,SAAS,qCAAqC;AAC9C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,kBAAkB;AAEpB,MAAM,mDAAmD,EAAE,OAAO;AAAA,EACvE,WAAW;AAAA,EACX,SAAS;AAAA,EACT,SAAS;AAAA,EACT,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,cAAc,EACX,KAAK,CAAC,OAAO,cAAc,gBAAgB,CAAC,EAC5C,SAAS,EACT,SAAS,4DAA4D;AAAA,EACxE,gBAAgB,EACb,OAAO,EACP,IAAI,CAAC,EACL,SAAS,EACT,SAAS,kEAAkE;AAAA,EAC9E,QAAQ,EACL,OAAO,EACP,IAAI,CAAC,EACL,IAAI,GAAG,EACP,SAAS,EACT,SAAS,yDAAyD;AAAA,EACrE,UAAU;AACZ,CAAC;AAED,eAAsB,kDACpB,QACA;AACA,MAAI;AACF,UAAM,UAAU,4BAA4B,OAAO,SAAS,OAAO,SAAS;AAC5E,UAAM,iBAAiB,OAAO,kBAAkB;AAChD,UAAM,SAAS,OAAO,UAAU;AAChC,UAAM,eAAe,OAAO,gBAAgB;AAC5C,UAAM,WAAW,MAAM;AAAA,MACrB;AAAA,MACA,4BAA4B;AAAA,QAC1B,WAAW,OAAO;AAAA,QAClB,SAAS,OAAO;AAAA,QAChB,YAAY,CAAC,OAAO;AAAA,QACpB,YAAY,OAAO;AAAA,QACnB,UAAU,OAAO,YAAY;AAAA,MAC/B,CAAC;AAAA,IACH;AAEA,UAAM,iBAAiB,SAAS,QAAQ,CAAC,GACtC,IAAI,CAAC,QAAQ,0BAA0B,KAAK,CAAC,OAAO,CAAC,CAAC,EACtD,IAAI,CAAC,SAAS;AAAA,MACb,OAAO,IAAI,WAAW;AAAA,MACtB,QAAQ,IAAI;AAAA,MACZ,aAAa,IAAI;AAAA,MACjB,aAAa,IAAI;AAAA,MACjB,UAAU,IAAI;AAAA,MACd,sBAAsB,mBAAmB,IAAI,WAAW,OAAO,OAAO,UAAU;AAAA,IAClF,EAAE,EACD,OAAO,CAAC,QAAQ,IAAI,eAAe,kBAAkB,IAAI,eAAe,MAAM,EAC9E,OAAO,CAAC,QAAQ;AACf,UAAI,iBAAiB,cAAc;AACjC,eAAO,IAAI,yBAAyB;AAAA,MACtC;AAEA,UAAI,iBAAiB,kBAAkB;AACrC,eAAO,IAAI,yBAAyB;AAAA,MACtC;AAEA,aAAO;AAAA,IACT,CAAC,EACA,IAAI,CAAC,SAAS;AAAA,MACb,GAAG;AAAA,MACH,mBAAmB,uCAAuC;AAAA,QACxD,aAAa,IAAI;AAAA,QACjB,YAAY,IAAI;AAAA,QAChB,UAAU,IAAI;AAAA,MAChB,CAAC;AAAA,MACD,QAAQ;AAAA,IACV,EAAE,EACD,KAAK,CAAC,MAAM,UAAU,MAAM,oBAAoB,KAAK,iBAAiB;AAEzE,WAAO;AAAA,MACL,WAAW;AAAA,QACT,UAAU;AAAA,QACV,YAAY;AAAA,UACV,YAAY,OAAO;AAAA,UACnB,UAAU,OAAO;AAAA,QACnB;AAAA,QACA,YAAY;AAAA,UACV,iBAAiB;AAAA,UACjB,iBAAiB;AAAA,QACnB;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF,SAAS,KAAK;AACZ,WAAO;AAAA,MACL,eAAe,QACX,IAAI,UACJ;AAAA,IACN;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -4,7 +4,7 @@ import { listSearchConsoleSites } from "../../services/search-console/search-con
4
4
  import {
5
5
  normalizePermissionLevel,
6
6
  normalizeSearchConsoleSiteEntry
7
- } from "../../search-console/search-console-utils.js";
7
+ } from "../../services/search-console/search-console-utils.js";
8
8
  import { stripNulls } from "../../utils/strip-payload.js";
9
9
  const listAccessibleSitesSchema = z.object({});
10
10
  async function listAccessibleSitesHandler(_params) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/tools/search-console/list-accessible-sites.ts"],
4
- "sourcesContent": ["import { error, object } from \"mcp-use/server\";\nimport { z } from \"zod\";\n\nimport { listSearchConsoleSites } from \"../../services/search-console/search-console-client.js\";\nimport {\n inferSearchConsolePropertyType,\n normalizePermissionLevel,\n normalizeSearchConsoleSiteEntry,\n} from \"../../search-console/search-console-utils.js\";\nimport { stripNulls } from \"../../utils/strip-payload.js\";\n\nexport const listAccessibleSitesSchema = z.object({});\n\nexport async function listAccessibleSitesHandler(\n _params: z.infer<typeof listAccessibleSitesSchema>\n) {\n try {\n const sites = await listSearchConsoleSites();\n const normalizedSites = sites\n .map(normalizeSearchConsoleSiteEntry)\n .sort((left, right) => left.site_url.localeCompare(right.site_url));\n\n const preferredDomainProperty = normalizedSites.find(\n (site) =>\n site.property_type === \"domain\" &&\n (site.permission_level === \"siteowner\" || site.permission_level === \"owner\")\n );\n\n return object(\n stripNulls({\n total_sites: normalizedSites.length,\n recommended_default_site_url: preferredDomainProperty?.site_url ?? normalizedSites[0]?.site_url,\n sites: normalizedSites,\n summary: {\n domain_properties: normalizedSites.filter((site) => site.property_type === \"domain\").length,\n url_prefix_properties: normalizedSites.filter((site) => site.property_type === \"url_prefix\").length,\n owner_level_sites: sites.filter((site) => {\n const permissionLevel = normalizePermissionLevel(site.permissionLevel);\n return permissionLevel === \"owner\" || permissionLevel === \"siteowner\";\n }).length,\n },\n })\n );\n } catch (err) {\n return error(\n err instanceof Error ? err.message : \"Failed to list accessible Search Console sites\"\n );\n }\n}\n"],
4
+ "sourcesContent": ["import { error, object } from \"mcp-use/server\";\nimport { z } from \"zod\";\n\nimport { listSearchConsoleSites } from \"../../services/search-console/search-console-client.js\";\nimport {\n inferSearchConsolePropertyType,\n normalizePermissionLevel,\n normalizeSearchConsoleSiteEntry,\n} from \"../../services/search-console/search-console-utils.js\";\nimport { stripNulls } from \"../../utils/strip-payload.js\";\n\nexport const listAccessibleSitesSchema = z.object({});\n\nexport async function listAccessibleSitesHandler(\n _params: z.infer<typeof listAccessibleSitesSchema>\n) {\n try {\n const sites = await listSearchConsoleSites();\n const normalizedSites = sites\n .map(normalizeSearchConsoleSiteEntry)\n .sort((left, right) => left.site_url.localeCompare(right.site_url));\n\n const preferredDomainProperty = normalizedSites.find(\n (site) =>\n site.property_type === \"domain\" &&\n (site.permission_level === \"siteowner\" || site.permission_level === \"owner\")\n );\n\n return object(\n stripNulls({\n total_sites: normalizedSites.length,\n recommended_default_site_url: preferredDomainProperty?.site_url ?? normalizedSites[0]?.site_url,\n sites: normalizedSites,\n summary: {\n domain_properties: normalizedSites.filter((site) => site.property_type === \"domain\").length,\n url_prefix_properties: normalizedSites.filter((site) => site.property_type === \"url_prefix\").length,\n owner_level_sites: sites.filter((site) => {\n const permissionLevel = normalizePermissionLevel(site.permissionLevel);\n return permissionLevel === \"owner\" || permissionLevel === \"siteowner\";\n }).length,\n },\n })\n );\n } catch (err) {\n return error(\n err instanceof Error ? err.message : \"Failed to list accessible Search Console sites\"\n );\n }\n}\n"],
5
5
  "mappings": "AAAA,SAAS,OAAO,cAAc;AAC9B,SAAS,SAAS;AAElB,SAAS,8BAA8B;AACvC;AAAA,EAEE;AAAA,EACA;AAAA,OACK;AACP,SAAS,kBAAkB;AAEpB,MAAM,4BAA4B,EAAE,OAAO,CAAC,CAAC;AAEpD,eAAsB,2BACpB,SACA;AACA,MAAI;AACF,UAAM,QAAQ,MAAM,uBAAuB;AAC3C,UAAM,kBAAkB,MACrB,IAAI,+BAA+B,EACnC,KAAK,CAAC,MAAM,UAAU,KAAK,SAAS,cAAc,MAAM,QAAQ,CAAC;AAEpE,UAAM,0BAA0B,gBAAgB;AAAA,MAC9C,CAAC,SACC,KAAK,kBAAkB,aACtB,KAAK,qBAAqB,eAAe,KAAK,qBAAqB;AAAA,IACxE;AAEA,WAAO;AAAA,MACL,WAAW;AAAA,QACT,aAAa,gBAAgB;AAAA,QAC7B,8BAA8B,yBAAyB,YAAY,gBAAgB,CAAC,GAAG;AAAA,QACvF,OAAO;AAAA,QACP,SAAS;AAAA,UACP,mBAAmB,gBAAgB,OAAO,CAAC,SAAS,KAAK,kBAAkB,QAAQ,EAAE;AAAA,UACrF,uBAAuB,gBAAgB,OAAO,CAAC,SAAS,KAAK,kBAAkB,YAAY,EAAE;AAAA,UAC7F,mBAAmB,MAAM,OAAO,CAAC,SAAS;AACxC,kBAAM,kBAAkB,yBAAyB,KAAK,eAAe;AACrE,mBAAO,oBAAoB,WAAW,oBAAoB;AAAA,UAC5D,CAAC,EAAE;AAAA,QACL;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF,SAAS,KAAK;AACZ,WAAO;AAAA,MACL,eAAe,QAAQ,IAAI,UAAU;AAAA,IACvC;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -13,7 +13,7 @@ import {
13
13
  siteUrlSchemaField,
14
14
  startDateSchemaField,
15
15
  normalizeSearchConsoleRow
16
- } from "../../search-console/search-console-utils.js";
16
+ } from "../../services/search-console/search-console-utils.js";
17
17
  import { stripNulls } from "../../utils/strip-payload.js";
18
18
  const searchConsoleLowCtrOpportunitiesSchema = z.object({
19
19
  startDate: startDateSchemaField,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/tools/search-console/low-ctr-opportunities.ts"],
4
- "sourcesContent": ["import { error, object } from \"mcp-use/server\";\nimport { z } from \"zod\";\n\nimport { querySearchConsolePerformance } from \"../../services/search-console/search-console-client.js\";\nimport {\n brandTermsSchemaField,\n buildSearchConsoleQueryBody,\n classifyBrandLabel,\n endDateSchemaField,\n profileIdSchemaField,\n resolveSearchConsoleSiteUrl,\n scoreQuickWinOpportunity,\n searchTypeSchemaField,\n siteUrlSchemaField,\n startDateSchemaField,\n normalizeSearchConsoleRow,\n} from \"../../search-console/search-console-utils.js\";\nimport { stripNulls } from \"../../utils/strip-payload.js\";\n\nexport const searchConsoleLowCtrOpportunitiesSchema = z.object({\n startDate: startDateSchemaField,\n endDate: endDateSchemaField,\n siteUrl: siteUrlSchemaField,\n profileId: profileIdSchemaField,\n searchType: searchTypeSchemaField,\n brandTerms: brandTermsSchemaField,\n includeBrand: z\n .enum([\"all\", \"only_brand\", \"only_non_brand\"])\n .optional()\n .describe(\"How to filter the query list when brandTerms are provided.\"),\n minImpressions: z\n .number()\n .min(1)\n .optional()\n .describe(\"Minimum impressions required to consider a CTR opportunity.\"),\n maxCtr: z\n .number()\n .min(0)\n .max(100)\n .optional()\n .describe(\"Maximum CTR percentage allowed to count as underperforming.\"),\n maxPosition: z\n .number()\n .min(1)\n .max(100)\n .optional()\n .describe(\"Maximum average position to keep only queries that already rank reasonably well.\"),\n});\n\nexport async function searchConsoleLowCtrOpportunitiesHandler(\n params: z.infer<typeof searchConsoleLowCtrOpportunitiesSchema>\n) {\n try {\n const siteUrl = resolveSearchConsoleSiteUrl(params.siteUrl, params.profileId);\n const minImpressions = params.minImpressions ?? 100;\n const maxCtr = params.maxCtr ?? 4;\n const maxPosition = params.maxPosition ?? 10;\n const includeBrand = params.includeBrand ?? \"all\";\n\n const response = await querySearchConsolePerformance(\n siteUrl,\n buildSearchConsoleQueryBody({\n startDate: params.startDate,\n endDate: params.endDate,\n dimensions: [\"query\"],\n searchType: params.searchType,\n rowLimit: 250,\n })\n );\n\n const opportunities = (response.rows ?? [])\n .map((row) => normalizeSearchConsoleRow(row, [\"query\"]))\n .map((row) => ({\n query: row.dimensions.query,\n clicks: row.clicks,\n impressions: row.impressions,\n ctr_percent: row.ctr_percent,\n position: row.position,\n brand_classification: classifyBrandLabel(row.dimensions.query, params.brandTerms),\n }))\n .filter(\n (row) =>\n row.impressions >= minImpressions &&\n row.ctr_percent <= maxCtr &&\n row.position > 0 &&\n row.position <= maxPosition\n )\n .filter((row) => {\n if (includeBrand === \"only_brand\") {\n return row.brand_classification === \"brand\";\n }\n\n if (includeBrand === \"only_non_brand\") {\n return row.brand_classification === \"non_brand\";\n }\n\n return true;\n })\n .map((row) => ({\n ...row,\n opportunity_score: scoreQuickWinOpportunity({\n impressions: row.impressions,\n ctrPercent: row.ctr_percent,\n position: row.position,\n }),\n reason: \"Ranking is already acceptable, so low CTR suggests snippet or title optimization potential.\",\n }))\n .sort((left, right) => right.opportunity_score - left.opportunity_score);\n\n return object(\n stripNulls({\n site_url: siteUrl,\n date_range: {\n start_date: params.startDate,\n end_date: params.endDate,\n },\n thresholds: {\n min_impressions: minImpressions,\n max_ctr_percent: maxCtr,\n max_position: maxPosition,\n },\n opportunities,\n })\n );\n } catch (err) {\n return error(\n err instanceof Error ? err.message : \"Failed to find Search Console low-CTR opportunities\"\n );\n }\n}\n"],
4
+ "sourcesContent": ["import { error, object } from \"mcp-use/server\";\nimport { z } from \"zod\";\n\nimport { querySearchConsolePerformance } from \"../../services/search-console/search-console-client.js\";\nimport {\n brandTermsSchemaField,\n buildSearchConsoleQueryBody,\n classifyBrandLabel,\n endDateSchemaField,\n profileIdSchemaField,\n resolveSearchConsoleSiteUrl,\n scoreQuickWinOpportunity,\n searchTypeSchemaField,\n siteUrlSchemaField,\n startDateSchemaField,\n normalizeSearchConsoleRow,\n} from \"../../services/search-console/search-console-utils.js\";\nimport { stripNulls } from \"../../utils/strip-payload.js\";\n\nexport const searchConsoleLowCtrOpportunitiesSchema = z.object({\n startDate: startDateSchemaField,\n endDate: endDateSchemaField,\n siteUrl: siteUrlSchemaField,\n profileId: profileIdSchemaField,\n searchType: searchTypeSchemaField,\n brandTerms: brandTermsSchemaField,\n includeBrand: z\n .enum([\"all\", \"only_brand\", \"only_non_brand\"])\n .optional()\n .describe(\"How to filter the query list when brandTerms are provided.\"),\n minImpressions: z\n .number()\n .min(1)\n .optional()\n .describe(\"Minimum impressions required to consider a CTR opportunity.\"),\n maxCtr: z\n .number()\n .min(0)\n .max(100)\n .optional()\n .describe(\"Maximum CTR percentage allowed to count as underperforming.\"),\n maxPosition: z\n .number()\n .min(1)\n .max(100)\n .optional()\n .describe(\"Maximum average position to keep only queries that already rank reasonably well.\"),\n});\n\nexport async function searchConsoleLowCtrOpportunitiesHandler(\n params: z.infer<typeof searchConsoleLowCtrOpportunitiesSchema>\n) {\n try {\n const siteUrl = resolveSearchConsoleSiteUrl(params.siteUrl, params.profileId);\n const minImpressions = params.minImpressions ?? 100;\n const maxCtr = params.maxCtr ?? 4;\n const maxPosition = params.maxPosition ?? 10;\n const includeBrand = params.includeBrand ?? \"all\";\n\n const response = await querySearchConsolePerformance(\n siteUrl,\n buildSearchConsoleQueryBody({\n startDate: params.startDate,\n endDate: params.endDate,\n dimensions: [\"query\"],\n searchType: params.searchType,\n rowLimit: 250,\n })\n );\n\n const opportunities = (response.rows ?? [])\n .map((row) => normalizeSearchConsoleRow(row, [\"query\"]))\n .map((row) => ({\n query: row.dimensions.query,\n clicks: row.clicks,\n impressions: row.impressions,\n ctr_percent: row.ctr_percent,\n position: row.position,\n brand_classification: classifyBrandLabel(row.dimensions.query, params.brandTerms),\n }))\n .filter(\n (row) =>\n row.impressions >= minImpressions &&\n row.ctr_percent <= maxCtr &&\n row.position > 0 &&\n row.position <= maxPosition\n )\n .filter((row) => {\n if (includeBrand === \"only_brand\") {\n return row.brand_classification === \"brand\";\n }\n\n if (includeBrand === \"only_non_brand\") {\n return row.brand_classification === \"non_brand\";\n }\n\n return true;\n })\n .map((row) => ({\n ...row,\n opportunity_score: scoreQuickWinOpportunity({\n impressions: row.impressions,\n ctrPercent: row.ctr_percent,\n position: row.position,\n }),\n reason: \"Ranking is already acceptable, so low CTR suggests snippet or title optimization potential.\",\n }))\n .sort((left, right) => right.opportunity_score - left.opportunity_score);\n\n return object(\n stripNulls({\n site_url: siteUrl,\n date_range: {\n start_date: params.startDate,\n end_date: params.endDate,\n },\n thresholds: {\n min_impressions: minImpressions,\n max_ctr_percent: maxCtr,\n max_position: maxPosition,\n },\n opportunities,\n })\n );\n } catch (err) {\n return error(\n err instanceof Error ? err.message : \"Failed to find Search Console low-CTR opportunities\"\n );\n }\n}\n"],
5
5
  "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -14,7 +14,7 @@ import {
14
14
  startRowSchemaField,
15
15
  sumSearchConsoleRows,
16
16
  normalizeSearchConsoleRow
17
- } from "../../search-console/search-console-utils.js";
17
+ } from "../../services/search-console/search-console-utils.js";
18
18
  import { stripNulls } from "../../utils/strip-payload.js";
19
19
  const searchConsolePagePerformanceSchema = z.object({
20
20
  startDate: startDateSchemaField,