@yoryoboy/bi-mcp 1.5.0 → 1.5.2

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 (86) hide show
  1. package/README.md +1 -2
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/mcp-use.json +2 -2
  4. package/dist/src/analytics/ga4-report-utils.js +20 -47
  5. package/dist/src/analytics/ga4-report-utils.js.map +2 -2
  6. package/dist/src/config/google-store.js +96 -0
  7. package/dist/src/config/google-store.js.map +7 -0
  8. package/dist/src/config/google.js +4 -15
  9. package/dist/src/config/google.js.map +2 -2
  10. package/dist/src/services/analytics/oauth.js +20 -7
  11. package/dist/src/services/analytics/oauth.js.map +2 -2
  12. package/dist/src/services/search-console/search-console-utils.js +49 -170
  13. package/dist/src/services/search-console/search-console-utils.js.map +2 -2
  14. package/dist/src/tools/analytics/attribution-gaps.js +3 -2
  15. package/dist/src/tools/analytics/attribution-gaps.js.map +2 -2
  16. package/dist/src/tools/analytics/channel-mix.js +3 -2
  17. package/dist/src/tools/analytics/channel-mix.js.map +2 -2
  18. package/dist/src/tools/analytics/ecommerce-tracking-health.js +3 -2
  19. package/dist/src/tools/analytics/ecommerce-tracking-health.js.map +2 -2
  20. package/dist/src/tools/analytics/engagement-overview.js +3 -2
  21. package/dist/src/tools/analytics/engagement-overview.js.map +2 -2
  22. package/dist/src/tools/analytics/property-info.js +3 -2
  23. package/dist/src/tools/analytics/property-info.js.map +2 -2
  24. package/dist/src/tools/analytics/revenue-by-channel.js +3 -2
  25. package/dist/src/tools/analytics/revenue-by-channel.js.map +2 -2
  26. package/dist/src/tools/analytics/revenue-overview.js +3 -2
  27. package/dist/src/tools/analytics/revenue-overview.js.map +2 -2
  28. package/dist/src/tools/analytics/revenue-trend.js +3 -2
  29. package/dist/src/tools/analytics/revenue-trend.js.map +2 -2
  30. package/dist/src/tools/analytics/source-medium-breakdown.js +3 -2
  31. package/dist/src/tools/analytics/source-medium-breakdown.js.map +2 -2
  32. package/dist/src/tools/analytics/top-landing-pages.js +3 -2
  33. package/dist/src/tools/analytics/top-landing-pages.js.map +2 -2
  34. package/dist/src/tools/config/list-profiles.js +23 -8
  35. package/dist/src/tools/config/list-profiles.js.map +2 -2
  36. package/dist/src/tools/google-ads/account-overview.js +3 -2
  37. package/dist/src/tools/google-ads/account-overview.js.map +2 -2
  38. package/dist/src/tools/google-ads/account-risks.js +3 -2
  39. package/dist/src/tools/google-ads/account-risks.js.map +2 -2
  40. package/dist/src/tools/google-ads/break-even-analysis.js +3 -2
  41. package/dist/src/tools/google-ads/break-even-analysis.js.map +2 -2
  42. package/dist/src/tools/google-ads/campaign-performance.js +3 -2
  43. package/dist/src/tools/google-ads/campaign-performance.js.map +2 -2
  44. package/dist/src/tools/google-ads/channel-mix.js +3 -2
  45. package/dist/src/tools/google-ads/channel-mix.js.map +2 -2
  46. package/dist/src/tools/google-ads/compare-accounts.js +1 -1
  47. package/dist/src/tools/google-ads/compare-accounts.js.map +2 -2
  48. package/dist/src/tools/google-ads/customer-clients.js +3 -2
  49. package/dist/src/tools/google-ads/customer-clients.js.map +2 -2
  50. package/dist/src/tools/google-ads/customer-info.js +3 -2
  51. package/dist/src/tools/google-ads/customer-info.js.map +2 -2
  52. package/dist/src/tools/google-ads/scaling-health.js +3 -2
  53. package/dist/src/tools/google-ads/scaling-health.js.map +2 -2
  54. package/dist/src/tools/google-ads/search-terms-summary.js +3 -2
  55. package/dist/src/tools/google-ads/search-terms-summary.js.map +2 -2
  56. package/dist/src/tools/google-ads/time-series.js +3 -2
  57. package/dist/src/tools/google-ads/time-series.js.map +2 -2
  58. package/dist/src/tools/search-console/country-breakdown.js +1 -1
  59. package/dist/src/tools/search-console/country-breakdown.js.map +2 -2
  60. package/dist/src/tools/search-console/device-breakdown.js +1 -1
  61. package/dist/src/tools/search-console/device-breakdown.js.map +2 -2
  62. package/dist/src/tools/search-console/high-impression-low-click-queries.js +1 -1
  63. package/dist/src/tools/search-console/high-impression-low-click-queries.js.map +2 -2
  64. package/dist/src/tools/search-console/low-ctr-opportunities.js +1 -1
  65. package/dist/src/tools/search-console/low-ctr-opportunities.js.map +2 -2
  66. package/dist/src/tools/search-console/page-performance.js +1 -1
  67. package/dist/src/tools/search-console/page-performance.js.map +2 -2
  68. package/dist/src/tools/search-console/product-demand-low-capture-queries.js +1 -1
  69. package/dist/src/tools/search-console/product-demand-low-capture-queries.js.map +2 -2
  70. package/dist/src/tools/search-console/query-page-matrix.js +1 -1
  71. package/dist/src/tools/search-console/query-page-matrix.js.map +2 -2
  72. package/dist/src/tools/search-console/query-performance.js +1 -1
  73. package/dist/src/tools/search-console/query-performance.js.map +2 -2
  74. package/dist/src/tools/search-console/quick-win-opportunities.js +1 -1
  75. package/dist/src/tools/search-console/quick-win-opportunities.js.map +2 -2
  76. package/dist/src/tools/search-console/rising-non-brand-queries.js +1 -1
  77. package/dist/src/tools/search-console/rising-non-brand-queries.js.map +2 -2
  78. package/dist/src/tools/search-console/search-performance.js +1 -1
  79. package/dist/src/tools/search-console/search-performance.js.map +2 -2
  80. package/dist/src/tools/search-console/site-context.js +2 -2
  81. package/dist/src/tools/search-console/site-context.js.map +2 -2
  82. package/dist/src/tools/search-console/visibility-declines.js +1 -1
  83. package/dist/src/tools/search-console/visibility-declines.js.map +2 -2
  84. package/dist/src/utils/google-ads.js +15 -26
  85. package/dist/src/utils/google-ads.js.map +2 -2
  86. package/package.json +1 -1
@@ -1,32 +1,13 @@
1
1
  import { z } from "zod";
2
+ import { getProfileGoogleServiceMapping } from "../../config/google-store.js";
2
3
  const searchConsoleDateRegex = /^\d{4}-\d{2}-\d{2}$/;
3
4
  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
- ];
5
+ const searchConsoleDimensions = ["query", "page", "country", "device", "date", "searchAppearance"];
6
+ const searchConsoleFilterOperators = ["contains", "equals", "notContains", "notEquals", "includingRegex", "excludingRegex"];
20
7
  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.");
8
+ const searchConsoleDimensionFilterSchema = z.object({ dimension: z.enum(searchConsoleDimensions).describe("Dimension to filter on in Search Console (query, page, country, device, date, or searchAppearance)"), operator: z.enum(searchConsoleFilterOperators).describe("Filter operator supported by Search Console."), expression: z.string().min(1).describe("Filter value or regex expression.") });
9
+ const siteUrlSchemaField = z.string().optional().describe("Search Console site URL or domain property identifier (for example https://www.example.com/ or sc-domain:example.com). If omitted, the tool tries profileId mapping first.");
10
+ const profileIdSchemaField = z.string().optional().describe("Optional business profile identifier used to resolve Google service mappings.");
30
11
  const startDateSchemaField = z.string().regex(searchConsoleDateRegex).describe("Start date in YYYY-MM-DD format.");
31
12
  const endDateSchemaField = z.string().regex(searchConsoleDateRegex).describe("End date in YYYY-MM-DD format.");
32
13
  const currentStartDateSchemaField = z.string().regex(searchConsoleDateRegex).describe("Current comparison period start date in YYYY-MM-DD format.");
@@ -37,49 +18,34 @@ const rowLimitSchemaField = z.number().int().min(1).max(25e3).optional().describ
37
18
  const startRowSchemaField = z.number().int().min(0).optional().describe("Zero-based row offset for paginated Search Console requests.");
38
19
  const searchTypeSchemaField = z.enum(searchConsoleSearchTypes).optional().describe("Search type to query. Defaults to web.");
39
20
  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) {
21
+ async function resolveSearchConsoleSiteUrl(siteUrl, profileId) {
41
22
  const explicitSiteUrl = siteUrl?.trim();
42
- if (explicitSiteUrl) {
43
- return explicitSiteUrl;
23
+ if (explicitSiteUrl) return explicitSiteUrl;
24
+ const explicitProfileId = profileId?.trim();
25
+ if (explicitProfileId) {
26
+ const mapping = await getProfileGoogleServiceMapping(explicitProfileId);
27
+ if (mapping?.searchConsoleSiteUrl) return mapping.searchConsoleSiteUrl;
28
+ throw new Error(`Missing Search Console site URL. profileId "${explicitProfileId}" has no Search Console mapping configured.`);
44
29
  }
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
- );
30
+ throw new Error("Missing Search Console site URL. Pass siteUrl explicitly, provide profileId with a configured mapping, or call gsc_list_accessible_sites first.");
53
31
  }
54
32
  function inferSearchConsolePropertyType(siteUrl) {
55
- if (siteUrl.startsWith("sc-domain:")) {
56
- return "domain";
57
- }
58
- if (/^https?:\/\//.test(siteUrl)) {
59
- return "url_prefix";
60
- }
33
+ if (siteUrl.startsWith("sc-domain:")) return "domain";
34
+ if (/^https?:\/\//.test(siteUrl)) return "url_prefix";
61
35
  return "unknown";
62
36
  }
63
37
  function normalizePermissionLevel(permissionLevel) {
64
38
  return permissionLevel?.trim().toLowerCase() || void 0;
65
39
  }
66
40
  function normalizeSearchConsoleSiteEntry(siteEntry) {
67
- return {
68
- site_url: siteEntry.siteUrl,
69
- property_type: inferSearchConsolePropertyType(siteEntry.siteUrl),
70
- permission_level: normalizePermissionLevel(siteEntry.permissionLevel)
71
- };
41
+ return { site_url: siteEntry.siteUrl, property_type: inferSearchConsolePropertyType(siteEntry.siteUrl), permission_level: normalizePermissionLevel(siteEntry.permissionLevel) };
72
42
  }
73
43
  function toPercent(numerator, denominator) {
74
- if (!Number.isFinite(numerator) || !Number.isFinite(denominator) || denominator <= 0) {
75
- return 0;
76
- }
44
+ if (!Number.isFinite(numerator) || !Number.isFinite(denominator) || denominator <= 0) return 0;
77
45
  return numerator / denominator * 100;
78
46
  }
79
47
  function round(value, decimals = 2) {
80
- if (!Number.isFinite(value)) {
81
- return 0;
82
- }
48
+ if (!Number.isFinite(value)) return 0;
83
49
  const factor = 10 ** decimals;
84
50
  return Math.round(value * factor) / factor;
85
51
  }
@@ -88,49 +54,21 @@ function normalizeSearchConsoleRow(row, dimensions = []) {
88
54
  const impressions = row.impressions ?? 0;
89
55
  const ctr = typeof row.ctr === "number" ? row.ctr * 100 : toPercent(clicks, impressions);
90
56
  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
- };
57
+ return { keys, dimensions: dimensions.reduce((accumulator, dimension, index) => {
58
+ accumulator[dimension] = keys[index] ?? "";
59
+ return accumulator;
60
+ }, {}), clicks: round(clicks, 2), impressions: round(impressions, 2), ctr_percent: round(ctr, 2), position: round(row.position ?? 0, 2) };
102
61
  }
103
62
  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
- };
63
+ const dimensionFilterGroups = input.dimensionFilters && input.dimensionFilters.length > 0 ? [{ groupType: "and", filters: input.dimensionFilters }] : void 0;
64
+ return { startDate: input.startDate, endDate: input.endDate, dimensions: input.dimensions, type: input.searchType ?? "web", dimensionFilterGroups, aggregationType: input.aggregationType, rowLimit: input.rowLimit, startRow: input.startRow };
120
65
  }
121
66
  function buildPaginationMetadata(input) {
122
67
  const startRow = input.startRow ?? 0;
123
68
  const rowLimit = input.rowLimit;
124
69
  const isPartial = Boolean(rowLimit) && input.returnedRows === rowLimit;
125
70
  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
- };
71
+ return { is_partial: isPartial, start_row: startRow, row_limit: rowLimit, returned_rows: input.returnedRows, next_start_row: nextStartRow, 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 };
134
72
  }
135
73
  function parseBrandTerms(brandTerms) {
136
74
  return (brandTerms ?? []).map((term) => term.trim().toLowerCase()).filter(Boolean);
@@ -138,98 +76,41 @@ function parseBrandTerms(brandTerms) {
138
76
  function isBrandedQuery(query, brandTerms) {
139
77
  const normalizedQuery = query.trim().toLowerCase();
140
78
  const normalizedBrandTerms = parseBrandTerms(brandTerms);
141
- if (!normalizedQuery || normalizedBrandTerms.length === 0) {
142
- return false;
143
- }
79
+ if (!normalizedQuery || normalizedBrandTerms.length === 0) return false;
144
80
  return normalizedBrandTerms.some((brandTerm) => normalizedQuery.includes(brandTerm));
145
81
  }
146
82
  function classifyBrandLabel(query, brandTerms) {
83
+ if ((brandTerms ?? []).length === 0) return "unknown";
147
84
  return isBrandedQuery(query, brandTerms) ? "brand" : "non_brand";
148
85
  }
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
86
  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
- };
87
+ const clicks = rows.reduce((total, row) => total + row.clicks, 0);
88
+ const impressions = rows.reduce((total, row) => total + row.impressions, 0);
89
+ const weightedCtr = impressions > 0 ? rows.reduce((total, row) => total + row.impressions * row.ctr_percent, 0) / impressions : 0;
90
+ const weightedPosition = impressions > 0 ? rows.reduce((total, row) => total + row.impressions * row.position, 0) / impressions : 0;
91
+ return { clicks: round(clicks), impressions: round(impressions), ctrPercent: round(weightedCtr), averagePosition: round(weightedPosition) };
181
92
  }
182
- function buildComparisonMap(rows, keyBuilder) {
183
- return new Map(rows.map((row) => [keyBuilder(row), row]));
93
+ function scoreQuickWinOpportunity(input) {
94
+ return round(input.impressions * Math.max(0, 8 - input.ctrPercent) * Math.max(0.5, 20 - input.position) / 100);
184
95
  }
185
- function tokenizeQuery(query) {
186
- return query.toLowerCase().split(/[^a-z0-9áéíóúüñ]+/i).map((token) => token.trim()).filter(Boolean);
96
+ function scoreHighImpressionLowClickOpportunity(input) {
97
+ return round(input.impressions * Math.max(0, 5 - input.ctrPercent) * Math.max(0.5, 15 - input.position) / 100);
187
98
  }
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));
99
+ function classifyQuickWinType(position, ctrPercent) {
100
+ if (position > 0 && position <= 10 && ctrPercent < 4) return "ctr";
101
+ if (position > 10 && position <= 20) return "position";
102
+ return "mixed";
207
103
  }
208
104
  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;
105
+ if ((productTerms ?? []).some((term) => query.toLowerCase().includes(term.trim().toLowerCase()))) return true;
106
+ return /\b(comprar|precio|modelo|sku|talle|color|review|opiniones)\b/i.test(query);
215
107
  }
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);
108
+ function buildComparisonMap(rows, getKey) {
109
+ return new Map(rows.map((row) => [getKey(row), row]));
227
110
  }
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);
111
+ function computeChangePercent(current, previous) {
112
+ if (previous === 0) return current > 0 ? 100 : 0;
113
+ return round((current - previous) / previous * 100);
233
114
  }
234
115
  export {
235
116
  brandTermsSchemaField,
@@ -244,7 +125,6 @@ export {
244
125
  endDateSchemaField,
245
126
  inferSearchConsolePropertyType,
246
127
  isBrandedQuery,
247
- looksLikeCommercialQuery,
248
128
  looksLikeProductQuery,
249
129
  normalizePermissionLevel,
250
130
  normalizeSearchConsoleRow,
@@ -269,7 +149,6 @@ export {
269
149
  startDateSchemaField,
270
150
  startRowSchemaField,
271
151
  sumSearchConsoleRows,
272
- toPercent,
273
- tokenizeQuery
152
+ toPercent
274
153
  };
275
154
  //# sourceMappingURL=search-console-utils.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
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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",
4
+ "sourcesContent": ["import { z } from 'zod';\nimport { getProfileGoogleServiceMapping } from '../../config/google-store.js';\nimport type { SearchConsoleDimensionFilter, SearchConsoleDimensionFilterGroup, SearchConsoleSearchAnalyticsRequest, SearchConsoleSearchAnalyticsRow, SearchConsoleSiteEntry } from './search-console-client.js';\n\nexport const searchConsoleDateRegex = /^\\d{4}-\\d{2}-\\d{2}$/;\nexport const searchConsoleSearchTypes = ['web', 'image', 'video', 'news', 'discover'] as const;\nexport const searchConsoleDimensions = ['query', 'page', 'country', 'device', 'date', 'searchAppearance'] as const;\nexport const searchConsoleFilterOperators = ['contains', 'equals', 'notContains', 'notEquals', 'includingRegex', 'excludingRegex'] as const;\nexport const searchConsoleAggregationTypes = ['auto', 'byPage', 'byProperty'] as const;\nexport const searchConsoleDimensionFilterSchema = z.object({ dimension: z.enum(searchConsoleDimensions).describe('Dimension to filter on in Search Console (query, page, country, device, date, or searchAppearance)'), operator: z.enum(searchConsoleFilterOperators).describe('Filter operator supported by Search Console.'), expression: z.string().min(1).describe('Filter value or regex expression.') });\nexport const siteUrlSchemaField = z.string().optional().describe('Search Console site URL or domain property identifier (for example https://www.example.com/ or sc-domain:example.com). If omitted, the tool tries profileId mapping first.');\nexport const profileIdSchemaField = z.string().optional().describe('Optional business profile identifier used to resolve Google service mappings.');\nexport const startDateSchemaField = z.string().regex(searchConsoleDateRegex).describe('Start date in YYYY-MM-DD format.');\nexport const endDateSchemaField = z.string().regex(searchConsoleDateRegex).describe('End date in YYYY-MM-DD format.');\nexport const currentStartDateSchemaField = z.string().regex(searchConsoleDateRegex).describe('Current comparison period start date in YYYY-MM-DD format.');\nexport const currentEndDateSchemaField = z.string().regex(searchConsoleDateRegex).describe('Current comparison period end date in YYYY-MM-DD format.');\nexport const previousStartDateSchemaField = z.string().regex(searchConsoleDateRegex).describe('Previous comparison period start date in YYYY-MM-DD format.');\nexport const previousEndDateSchemaField = z.string().regex(searchConsoleDateRegex).describe('Previous comparison period end date in YYYY-MM-DD format.');\nexport const rowLimitSchemaField = z.number().int().min(1).max(25000).optional().describe('Maximum number of rows to return. Search Console supports up to 25,000 rows per request.');\nexport const startRowSchemaField = z.number().int().min(0).optional().describe('Zero-based row offset for paginated Search Console requests.');\nexport const searchTypeSchemaField = z.enum(searchConsoleSearchTypes).optional().describe('Search type to query. Defaults to web.');\nexport const brandTermsSchemaField = z.array(z.string().min(1)).max(50).optional().describe('Brand tokens used to separate branded from non-branded queries.');\n\nexport async function resolveSearchConsoleSiteUrl(siteUrl?: string, profileId?: string): Promise<string> {\n const explicitSiteUrl = siteUrl?.trim();\n if (explicitSiteUrl) return explicitSiteUrl;\n const explicitProfileId = profileId?.trim();\n if (explicitProfileId) {\n const mapping = await getProfileGoogleServiceMapping(explicitProfileId);\n if (mapping?.searchConsoleSiteUrl) return mapping.searchConsoleSiteUrl;\n throw new Error(`Missing Search Console site URL. profileId \"${explicitProfileId}\" has no Search Console mapping configured.`);\n }\n throw new Error('Missing Search Console site URL. Pass siteUrl explicitly, provide profileId with a configured mapping, or call gsc_list_accessible_sites first.');\n}\n\nexport function inferSearchConsolePropertyType(siteUrl: string): 'domain' | 'url_prefix' | 'unknown' { if (siteUrl.startsWith('sc-domain:')) return 'domain'; if (/^https?:\\/\\//.test(siteUrl)) return 'url_prefix'; return 'unknown'; }\nexport function normalizePermissionLevel(permissionLevel?: string): string | undefined { return permissionLevel?.trim().toLowerCase() || undefined; }\nexport function normalizeSearchConsoleSiteEntry(siteEntry: SearchConsoleSiteEntry) { return { site_url: siteEntry.siteUrl, property_type: inferSearchConsolePropertyType(siteEntry.siteUrl), permission_level: normalizePermissionLevel(siteEntry.permissionLevel) }; }\nexport function toPercent(numerator: number, denominator: number): number { if (!Number.isFinite(numerator) || !Number.isFinite(denominator) || denominator <= 0) return 0; return (numerator / denominator) * 100; }\nexport function round(value: number, decimals = 2): number { if (!Number.isFinite(value)) return 0; const factor = 10 ** decimals; return Math.round(value * factor) / factor; }\nexport function normalizeSearchConsoleRow(row: SearchConsoleSearchAnalyticsRow, dimensions: readonly string[] = []) { const clicks = row.clicks ?? 0; const impressions = row.impressions ?? 0; const ctr = typeof row.ctr === 'number' ? row.ctr * 100 : toPercent(clicks, impressions); const keys = row.keys ?? []; return { keys, dimensions: dimensions.reduce<Record<string, string>>((accumulator, dimension, index) => { accumulator[dimension] = keys[index] ?? ''; return accumulator; }, {}), clicks: round(clicks, 2), impressions: round(impressions, 2), ctr_percent: round(ctr, 2), position: round(row.position ?? 0, 2) }; }\nexport function buildSearchConsoleQueryBody(input: { startDate: string; endDate: string; dimensions?: string[]; searchType?: string; dimensionFilters?: SearchConsoleDimensionFilter[]; aggregationType?: string; rowLimit?: number; startRow?: number; }): SearchConsoleSearchAnalyticsRequest { const dimensionFilterGroups: SearchConsoleDimensionFilterGroup[] | undefined = input.dimensionFilters && input.dimensionFilters.length > 0 ? [{ groupType: 'and', filters: input.dimensionFilters }] : undefined; return { startDate: input.startDate, endDate: input.endDate, dimensions: input.dimensions, type: input.searchType ?? 'web', dimensionFilterGroups, aggregationType: input.aggregationType, rowLimit: input.rowLimit, startRow: input.startRow }; }\nexport function buildPaginationMetadata(input: { startRow?: number; rowLimit?: number; returnedRows: number; }) { const startRow = input.startRow ?? 0; const rowLimit = input.rowLimit; const isPartial = Boolean(rowLimit) && input.returnedRows === rowLimit; const nextStartRow = isPartial ? startRow + input.returnedRows : undefined; return { is_partial: isPartial, start_row: startRow, row_limit: rowLimit, returned_rows: input.returnedRows, next_start_row: nextStartRow, 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.' : undefined }; }\nexport function parseBrandTerms(brandTerms?: string[]): string[] { return (brandTerms ?? []).map((term) => term.trim().toLowerCase()).filter(Boolean); }\nexport function isBrandedQuery(query: string, brandTerms?: string[]): boolean { const normalizedQuery = query.trim().toLowerCase(); const normalizedBrandTerms = parseBrandTerms(brandTerms); if (!normalizedQuery || normalizedBrandTerms.length === 0) return false; return normalizedBrandTerms.some((brandTerm) => normalizedQuery.includes(brandTerm)); }\nexport function classifyBrandLabel(query: string, brandTerms?: string[]) { if ((brandTerms ?? []).length === 0) return 'unknown'; return isBrandedQuery(query, brandTerms) ? 'brand' : 'non_brand'; }\nexport function sumSearchConsoleRows(rows: Array<{ clicks: number; impressions: number; ctr_percent: number; position: number }>) { const clicks = rows.reduce((total, row) => total + row.clicks, 0); const impressions = rows.reduce((total, row) => total + row.impressions, 0); const weightedCtr = impressions > 0 ? rows.reduce((total, row) => total + row.impressions * row.ctr_percent, 0) / impressions : 0; const weightedPosition = impressions > 0 ? rows.reduce((total, row) => total + row.impressions * row.position, 0) / impressions : 0; return { clicks: round(clicks), impressions: round(impressions), ctrPercent: round(weightedCtr), averagePosition: round(weightedPosition) }; }\nexport function scoreQuickWinOpportunity(input: { impressions: number; ctrPercent: number; position: number }) { return round(input.impressions * Math.max(0, 8 - input.ctrPercent) * Math.max(0.5, 20 - input.position) / 100); }\nexport function scoreHighImpressionLowClickOpportunity(input: { impressions: number; ctrPercent: number; position: number }) { return round(input.impressions * Math.max(0, 5 - input.ctrPercent) * Math.max(0.5, 15 - input.position) / 100); }\nexport function classifyQuickWinType(position: number, ctrPercent: number) { if (position > 0 && position <= 10 && ctrPercent < 4) return 'ctr'; if (position > 10 && position <= 20) return 'position'; return 'mixed'; }\nexport function looksLikeProductQuery(query: string, productTerms?: string[]) { if ((productTerms ?? []).some((term) => query.toLowerCase().includes(term.trim().toLowerCase()))) return true; return /\\b(comprar|precio|modelo|sku|talle|color|review|opiniones)\\b/i.test(query); }\nexport function buildComparisonMap<T>(rows: T[], getKey: (row: T) => string) { return new Map(rows.map((row) => [getKey(row), row])); }\nexport function computeChangePercent(current: number, previous: number) { if (previous === 0) return current > 0 ? 100 : 0; return round(((current - previous) / previous) * 100); }\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -16,12 +16,13 @@ const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
16
16
  const attributionGapsSchema = z.object({
17
17
  startDate: z.string().regex(dateRegex).describe("Start date in YYYY-MM-DD format"),
18
18
  endDate: z.string().regex(dateRegex).describe("End date in YYYY-MM-DD format"),
19
- propertyId: z.string().optional().describe("GA4 property ID. If omitted, uses GA4_PROPERTY_ID"),
19
+ propertyId: z.string().optional().describe("GA4 property ID. If omitted, resolves profileId mapping first and then GA4_PROPERTY_ID."),
20
+ profileId: z.string().optional().describe("Optional business profile ID used to resolve the mapped GA4 property."),
20
21
  limit: z.number().int().min(10).max(100).optional().describe("Maximum source / medium rows to inspect")
21
22
  });
22
23
  async function attributionGapsHandler(params) {
23
24
  try {
24
- const propertyId = resolveGa4PropertyId(params.propertyId);
25
+ const propertyId = await resolveGa4PropertyId(params.propertyId, params.profileId);
25
26
  const limit = params.limit ?? 50;
26
27
  const [channelReport, sourceMediumReport, totalReport] = await Promise.all([
27
28
  runReport(propertyId, {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/tools/analytics/attribution-gaps.ts"],
4
- "sourcesContent": ["import { error, object } from \"mcp-use/server\";\nimport { z } from \"zod\";\n\nimport {\n detectSeverity,\n extractQuotaSnapshot,\n getDimensionValue,\n getMetricValueByName,\n isProblematicAttributionValue,\n resolveGa4PropertyId,\n round,\n toPercent,\n} from \"../../analytics/ga4-report-utils.js\";\nimport { runReport } from \"../../services/analytics/ga4-client.js\";\nimport { stripNulls } from \"../../utils/strip-payload.js\";\n\nconst dateRegex = /^\\d{4}-\\d{2}-\\d{2}$/;\n\nexport const attributionGapsSchema = z.object({\n startDate: z.string().regex(dateRegex).describe(\"Start date in YYYY-MM-DD format\"),\n endDate: z.string().regex(dateRegex).describe(\"End date in YYYY-MM-DD format\"),\n propertyId: z.string().optional().describe(\"GA4 property ID. If omitted, uses GA4_PROPERTY_ID\"),\n limit: z.number().int().min(10).max(100).optional().describe(\"Maximum source / medium rows to inspect\"),\n});\n\nexport async function attributionGapsHandler(params: z.infer<typeof attributionGapsSchema>) {\n try {\n const propertyId = resolveGa4PropertyId(params.propertyId);\n const limit = params.limit ?? 50;\n\n const [channelReport, sourceMediumReport, totalReport] = await Promise.all([\n runReport(propertyId, {\n dateRanges: [{ startDate: params.startDate, endDate: params.endDate }],\n dimensions: [{ name: \"sessionDefaultChannelGroup\" }],\n metrics: [{ name: \"sessions\" }, { name: \"totalUsers\" }],\n }),\n runReport(propertyId, {\n dateRanges: [{ startDate: params.startDate, endDate: params.endDate }],\n dimensions: [{ name: \"sessionSourceMedium\" }],\n metrics: [{ name: \"sessions\" }, { name: \"totalUsers\" }],\n orderBys: [{ metric: { metricName: \"sessions\" }, desc: true }],\n limit,\n returnPropertyQuota: true,\n }),\n runReport(propertyId, {\n dateRanges: [{ startDate: params.startDate, endDate: params.endDate }],\n metrics: [{ name: \"sessions\" }],\n }),\n ]);\n\n const totalSessions = getMetricValueByName(\n totalReport.rows?.[0] ?? {},\n totalReport.metricHeaders,\n \"sessions\"\n );\n\n const unassignedChannel = (channelReport.rows ?? []).find(\n (row) => getDimensionValue(row) === \"Unassigned\"\n );\n const unassignedSessions = getMetricValueByName(\n unassignedChannel ?? {},\n channelReport.metricHeaders,\n \"sessions\"\n );\n\n const problematicSourceMediumRows = (sourceMediumReport.rows ?? [])\n .filter((row) => isProblematicAttributionValue(getDimensionValue(row)))\n .map((row) => {\n const sessions = getMetricValueByName(row, sourceMediumReport.metricHeaders, \"sessions\");\n const users = getMetricValueByName(row, sourceMediumReport.metricHeaders, \"totalUsers\");\n\n return {\n source_medium: getDimensionValue(row),\n sessions,\n users,\n session_share_percent: round(toPercent(sessions, totalSessions)),\n };\n });\n\n const problematicSourceMediumSessions = problematicSourceMediumRows.reduce(\n (total, row) => total + row.sessions,\n 0\n );\n const unassignedSharePercent = round(toPercent(unassignedSessions, totalSessions));\n const problematicSourceMediumSharePercent = round(\n toPercent(problematicSourceMediumSessions, totalSessions)\n );\n\n return object(\n stripNulls({\n property_id: propertyId,\n date_range: {\n start_date: params.startDate,\n end_date: params.endDate,\n },\n overview: {\n total_sessions: totalSessions,\n attribution_warning: \"Las se\u00F1ales se reportan por separado para evitar doble conteo entre dimensiones distintas de GA4.\",\n },\n unassigned_channel_group: {\n sessions: unassignedSessions,\n session_share_percent: unassignedSharePercent,\n severity: detectSeverity(unassignedSharePercent),\n },\n problematic_source_medium: {\n sessions: problematicSourceMediumSessions,\n session_share_percent: problematicSourceMediumSharePercent,\n severity: detectSeverity(problematicSourceMediumSharePercent),\n },\n problematic_source_mediums: problematicSourceMediumRows,\n quota: extractQuotaSnapshot(sourceMediumReport),\n })\n );\n } catch (err) {\n return error(err instanceof Error ? err.message : \"Failed to detect GA4 attribution gaps\");\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { error, object } from \"mcp-use/server\";\nimport { z } from \"zod\";\n\nimport {\n detectSeverity,\n extractQuotaSnapshot,\n getDimensionValue,\n getMetricValueByName,\n isProblematicAttributionValue,\n resolveGa4PropertyId,\n round,\n toPercent,\n} from \"../../analytics/ga4-report-utils.js\";\nimport { runReport } from \"../../services/analytics/ga4-client.js\";\nimport { stripNulls } from \"../../utils/strip-payload.js\";\n\nconst dateRegex = /^\\d{4}-\\d{2}-\\d{2}$/;\n\nexport const attributionGapsSchema = z.object({\n startDate: z.string().regex(dateRegex).describe(\"Start date in YYYY-MM-DD format\"),\n endDate: z.string().regex(dateRegex).describe(\"End date in YYYY-MM-DD format\"),\n propertyId: z.string().optional().describe(\"GA4 property ID. If omitted, resolves profileId mapping first and then GA4_PROPERTY_ID.\"),\n profileId: z.string().optional().describe(\"Optional business profile ID used to resolve the mapped GA4 property.\"),\n limit: z.number().int().min(10).max(100).optional().describe(\"Maximum source / medium rows to inspect\"),\n});\n\nexport async function attributionGapsHandler(params: z.infer<typeof attributionGapsSchema>) {\n try {\n const propertyId = await resolveGa4PropertyId(params.propertyId, params.profileId);\n const limit = params.limit ?? 50;\n\n const [channelReport, sourceMediumReport, totalReport] = await Promise.all([\n runReport(propertyId, {\n dateRanges: [{ startDate: params.startDate, endDate: params.endDate }],\n dimensions: [{ name: \"sessionDefaultChannelGroup\" }],\n metrics: [{ name: \"sessions\" }, { name: \"totalUsers\" }],\n }),\n runReport(propertyId, {\n dateRanges: [{ startDate: params.startDate, endDate: params.endDate }],\n dimensions: [{ name: \"sessionSourceMedium\" }],\n metrics: [{ name: \"sessions\" }, { name: \"totalUsers\" }],\n orderBys: [{ metric: { metricName: \"sessions\" }, desc: true }],\n limit,\n returnPropertyQuota: true,\n }),\n runReport(propertyId, {\n dateRanges: [{ startDate: params.startDate, endDate: params.endDate }],\n metrics: [{ name: \"sessions\" }],\n }),\n ]);\n\n const totalSessions = getMetricValueByName(\n totalReport.rows?.[0] ?? {},\n totalReport.metricHeaders,\n \"sessions\"\n );\n\n const unassignedChannel = (channelReport.rows ?? []).find(\n (row) => getDimensionValue(row) === \"Unassigned\"\n );\n const unassignedSessions = getMetricValueByName(\n unassignedChannel ?? {},\n channelReport.metricHeaders,\n \"sessions\"\n );\n\n const problematicSourceMediumRows = (sourceMediumReport.rows ?? [])\n .filter((row) => isProblematicAttributionValue(getDimensionValue(row)))\n .map((row) => {\n const sessions = getMetricValueByName(row, sourceMediumReport.metricHeaders, \"sessions\");\n const users = getMetricValueByName(row, sourceMediumReport.metricHeaders, \"totalUsers\");\n\n return {\n source_medium: getDimensionValue(row),\n sessions,\n users,\n session_share_percent: round(toPercent(sessions, totalSessions)),\n };\n });\n\n const problematicSourceMediumSessions = problematicSourceMediumRows.reduce(\n (total, row) => total + row.sessions,\n 0\n );\n const unassignedSharePercent = round(toPercent(unassignedSessions, totalSessions));\n const problematicSourceMediumSharePercent = round(\n toPercent(problematicSourceMediumSessions, totalSessions)\n );\n\n return object(\n stripNulls({\n property_id: propertyId,\n date_range: {\n start_date: params.startDate,\n end_date: params.endDate,\n },\n overview: {\n total_sessions: totalSessions,\n attribution_warning: \"Las se\u00F1ales se reportan por separado para evitar doble conteo entre dimensiones distintas de GA4.\",\n },\n unassigned_channel_group: {\n sessions: unassignedSessions,\n session_share_percent: unassignedSharePercent,\n severity: detectSeverity(unassignedSharePercent),\n },\n problematic_source_medium: {\n sessions: problematicSourceMediumSessions,\n session_share_percent: problematicSourceMediumSharePercent,\n severity: detectSeverity(problematicSourceMediumSharePercent),\n },\n problematic_source_mediums: problematicSourceMediumRows,\n quota: extractQuotaSnapshot(sourceMediumReport),\n })\n );\n } catch (err) {\n return error(err instanceof Error ? err.message : \"Failed to detect GA4 attribution gaps\");\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -16,11 +16,12 @@ const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
16
16
  const channelMixSchema = z.object({
17
17
  startDate: z.string().regex(dateRegex).describe("Start date in YYYY-MM-DD format"),
18
18
  endDate: z.string().regex(dateRegex).describe("End date in YYYY-MM-DD format"),
19
- propertyId: z.string().optional().describe("GA4 property ID. If omitted, uses GA4_PROPERTY_ID")
19
+ propertyId: z.string().optional().describe("GA4 property ID. If omitted, resolves profileId mapping first and then GA4_PROPERTY_ID."),
20
+ profileId: z.string().optional().describe("Optional business profile ID used to resolve the mapped GA4 property.")
20
21
  });
21
22
  async function channelMixHandler(params) {
22
23
  try {
23
- const propertyId = resolveGa4PropertyId(params.propertyId);
24
+ const propertyId = await resolveGa4PropertyId(params.propertyId, params.profileId);
24
25
  const report = await runReport(propertyId, {
25
26
  dateRanges: [{ startDate: params.startDate, endDate: params.endDate }],
26
27
  dimensions: [{ name: "sessionDefaultChannelGroup" }],
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/tools/analytics/channel-mix.ts"],
4
- "sourcesContent": ["import { error, object } from \"mcp-use/server\";\nimport { z } from \"zod\";\n\nimport {\n extractQuotaSnapshot,\n getDimensionValue,\n getMetricValueByName,\n resolveGa4PropertyId,\n round,\n sumMetricValues,\n toPercent,\n} from \"../../analytics/ga4-report-utils.js\";\nimport { isPaidDefaultChannelGroup } from \"../../analytics/ga4-channel-groups.js\";\nimport { runReport } from \"../../services/analytics/ga4-client.js\";\nimport { stripNulls } from \"../../utils/strip-payload.js\";\n\nconst dateRegex = /^\\d{4}-\\d{2}-\\d{2}$/;\n\nexport const channelMixSchema = z.object({\n startDate: z.string().regex(dateRegex).describe(\"Start date in YYYY-MM-DD format\"),\n endDate: z.string().regex(dateRegex).describe(\"End date in YYYY-MM-DD format\"),\n propertyId: z.string().optional().describe(\"GA4 property ID. If omitted, uses GA4_PROPERTY_ID\"),\n});\n\nexport async function channelMixHandler(params: z.infer<typeof channelMixSchema>) {\n try {\n const propertyId = resolveGa4PropertyId(params.propertyId);\n\n const report = await runReport(propertyId, {\n dateRanges: [{ startDate: params.startDate, endDate: params.endDate }],\n dimensions: [{ name: \"sessionDefaultChannelGroup\" }],\n metrics: [{ name: \"sessions\" }, { name: \"totalUsers\" }],\n orderBys: [{ metric: { metricName: \"sessions\" }, desc: true }],\n returnPropertyQuota: true,\n });\n\n const totalSessions = sumMetricValues(report.rows, report.metricHeaders, \"sessions\");\n const totalUsers = sumMetricValues(report.rows, report.metricHeaders, \"totalUsers\");\n\n const channels = (report.rows ?? []).map((row) => {\n const sessions = getMetricValueByName(row, report.metricHeaders, \"sessions\");\n const users = getMetricValueByName(row, report.metricHeaders, \"totalUsers\");\n\n return {\n channel: getDimensionValue(row),\n sessions,\n users,\n session_share_percent: round(toPercent(sessions, totalSessions)),\n user_share_percent: round(toPercent(users, totalUsers)),\n };\n });\n\n const paidSessions = channels\n .filter((channel) => isPaidDefaultChannelGroup(channel.channel))\n .reduce((total, channel) => total + channel.sessions, 0);\n const organicSessions = channels\n .filter((channel) => channel.channel.startsWith(\"Organic\"))\n .reduce((total, channel) => total + channel.sessions, 0);\n const directSessions = channels\n .filter((channel) => channel.channel === \"Direct\")\n .reduce((total, channel) => total + channel.sessions, 0);\n\n return object(\n stripNulls({\n property_id: propertyId,\n date_range: {\n start_date: params.startDate,\n end_date: params.endDate,\n },\n overview: {\n total_sessions: totalSessions,\n total_users: totalUsers,\n top_channel: channels[0]?.channel,\n paid_session_share_percent: round(toPercent(paidSessions, totalSessions)),\n organic_session_share_percent: round(toPercent(organicSessions, totalSessions)),\n direct_session_share_percent: round(toPercent(directSessions, totalSessions)),\n },\n channels,\n quota: extractQuotaSnapshot(report),\n })\n );\n } catch (err) {\n return error(err instanceof Error ? err.message : \"Failed to fetch GA4 channel mix\");\n }\n}\n"],
5
- "mappings": "AAAA,SAAS,OAAO,cAAc;AAC9B,SAAS,SAAS;AAElB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,iCAAiC;AAC1C,SAAS,iBAAiB;AAC1B,SAAS,kBAAkB;AAE3B,MAAM,YAAY;AAEX,MAAM,mBAAmB,EAAE,OAAO;AAAA,EACvC,WAAW,EAAE,OAAO,EAAE,MAAM,SAAS,EAAE,SAAS,iCAAiC;AAAA,EACjF,SAAS,EAAE,OAAO,EAAE,MAAM,SAAS,EAAE,SAAS,+BAA+B;AAAA,EAC7E,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,mDAAmD;AAChG,CAAC;AAED,eAAsB,kBAAkB,QAA0C;AAChF,MAAI;AACF,UAAM,aAAa,qBAAqB,OAAO,UAAU;AAEzD,UAAM,SAAS,MAAM,UAAU,YAAY;AAAA,MACzC,YAAY,CAAC,EAAE,WAAW,OAAO,WAAW,SAAS,OAAO,QAAQ,CAAC;AAAA,MACrE,YAAY,CAAC,EAAE,MAAM,6BAA6B,CAAC;AAAA,MACnD,SAAS,CAAC,EAAE,MAAM,WAAW,GAAG,EAAE,MAAM,aAAa,CAAC;AAAA,MACtD,UAAU,CAAC,EAAE,QAAQ,EAAE,YAAY,WAAW,GAAG,MAAM,KAAK,CAAC;AAAA,MAC7D,qBAAqB;AAAA,IACvB,CAAC;AAED,UAAM,gBAAgB,gBAAgB,OAAO,MAAM,OAAO,eAAe,UAAU;AACnF,UAAM,aAAa,gBAAgB,OAAO,MAAM,OAAO,eAAe,YAAY;AAElF,UAAM,YAAY,OAAO,QAAQ,CAAC,GAAG,IAAI,CAAC,QAAQ;AAChD,YAAM,WAAW,qBAAqB,KAAK,OAAO,eAAe,UAAU;AAC3E,YAAM,QAAQ,qBAAqB,KAAK,OAAO,eAAe,YAAY;AAE1E,aAAO;AAAA,QACL,SAAS,kBAAkB,GAAG;AAAA,QAC9B;AAAA,QACA;AAAA,QACA,uBAAuB,MAAM,UAAU,UAAU,aAAa,CAAC;AAAA,QAC/D,oBAAoB,MAAM,UAAU,OAAO,UAAU,CAAC;AAAA,MACxD;AAAA,IACF,CAAC;AAED,UAAM,eAAe,SAClB,OAAO,CAAC,YAAY,0BAA0B,QAAQ,OAAO,CAAC,EAC9D,OAAO,CAAC,OAAO,YAAY,QAAQ,QAAQ,UAAU,CAAC;AACzD,UAAM,kBAAkB,SACrB,OAAO,CAAC,YAAY,QAAQ,QAAQ,WAAW,SAAS,CAAC,EACzD,OAAO,CAAC,OAAO,YAAY,QAAQ,QAAQ,UAAU,CAAC;AACzD,UAAM,iBAAiB,SACpB,OAAO,CAAC,YAAY,QAAQ,YAAY,QAAQ,EAChD,OAAO,CAAC,OAAO,YAAY,QAAQ,QAAQ,UAAU,CAAC;AAEzD,WAAO;AAAA,MACL,WAAW;AAAA,QACT,aAAa;AAAA,QACb,YAAY;AAAA,UACV,YAAY,OAAO;AAAA,UACnB,UAAU,OAAO;AAAA,QACnB;AAAA,QACA,UAAU;AAAA,UACR,gBAAgB;AAAA,UAChB,aAAa;AAAA,UACb,aAAa,SAAS,CAAC,GAAG;AAAA,UAC1B,4BAA4B,MAAM,UAAU,cAAc,aAAa,CAAC;AAAA,UACxE,+BAA+B,MAAM,UAAU,iBAAiB,aAAa,CAAC;AAAA,UAC9E,8BAA8B,MAAM,UAAU,gBAAgB,aAAa,CAAC;AAAA,QAC9E;AAAA,QACA;AAAA,QACA,OAAO,qBAAqB,MAAM;AAAA,MACpC,CAAC;AAAA,IACH;AAAA,EACF,SAAS,KAAK;AACZ,WAAO,MAAM,eAAe,QAAQ,IAAI,UAAU,iCAAiC;AAAA,EACrF;AACF;",
4
+ "sourcesContent": ["import { error, object } from \"mcp-use/server\";\nimport { z } from \"zod\";\n\nimport {\n extractQuotaSnapshot,\n getDimensionValue,\n getMetricValueByName,\n resolveGa4PropertyId,\n round,\n sumMetricValues,\n toPercent,\n} from \"../../analytics/ga4-report-utils.js\";\nimport { isPaidDefaultChannelGroup } from \"../../analytics/ga4-channel-groups.js\";\nimport { runReport } from \"../../services/analytics/ga4-client.js\";\nimport { stripNulls } from \"../../utils/strip-payload.js\";\n\nconst dateRegex = /^\\d{4}-\\d{2}-\\d{2}$/;\n\nexport const channelMixSchema = z.object({\n startDate: z.string().regex(dateRegex).describe(\"Start date in YYYY-MM-DD format\"),\n endDate: z.string().regex(dateRegex).describe(\"End date in YYYY-MM-DD format\"),\n propertyId: z.string().optional().describe(\"GA4 property ID. If omitted, resolves profileId mapping first and then GA4_PROPERTY_ID.\"),\n profileId: z.string().optional().describe(\"Optional business profile ID used to resolve the mapped GA4 property.\"),\n});\n\nexport async function channelMixHandler(params: z.infer<typeof channelMixSchema>) {\n try {\n const propertyId = await resolveGa4PropertyId(params.propertyId, params.profileId);\n\n const report = await runReport(propertyId, {\n dateRanges: [{ startDate: params.startDate, endDate: params.endDate }],\n dimensions: [{ name: \"sessionDefaultChannelGroup\" }],\n metrics: [{ name: \"sessions\" }, { name: \"totalUsers\" }],\n orderBys: [{ metric: { metricName: \"sessions\" }, desc: true }],\n returnPropertyQuota: true,\n });\n\n const totalSessions = sumMetricValues(report.rows, report.metricHeaders, \"sessions\");\n const totalUsers = sumMetricValues(report.rows, report.metricHeaders, \"totalUsers\");\n\n const channels = (report.rows ?? []).map((row) => {\n const sessions = getMetricValueByName(row, report.metricHeaders, \"sessions\");\n const users = getMetricValueByName(row, report.metricHeaders, \"totalUsers\");\n\n return {\n channel: getDimensionValue(row),\n sessions,\n users,\n session_share_percent: round(toPercent(sessions, totalSessions)),\n user_share_percent: round(toPercent(users, totalUsers)),\n };\n });\n\n const paidSessions = channels\n .filter((channel) => isPaidDefaultChannelGroup(channel.channel))\n .reduce((total, channel) => total + channel.sessions, 0);\n const organicSessions = channels\n .filter((channel) => channel.channel.startsWith(\"Organic\"))\n .reduce((total, channel) => total + channel.sessions, 0);\n const directSessions = channels\n .filter((channel) => channel.channel === \"Direct\")\n .reduce((total, channel) => total + channel.sessions, 0);\n\n return object(\n stripNulls({\n property_id: propertyId,\n date_range: {\n start_date: params.startDate,\n end_date: params.endDate,\n },\n overview: {\n total_sessions: totalSessions,\n total_users: totalUsers,\n top_channel: channels[0]?.channel,\n paid_session_share_percent: round(toPercent(paidSessions, totalSessions)),\n organic_session_share_percent: round(toPercent(organicSessions, totalSessions)),\n direct_session_share_percent: round(toPercent(directSessions, totalSessions)),\n },\n channels,\n quota: extractQuotaSnapshot(report),\n })\n );\n } catch (err) {\n return error(err instanceof Error ? err.message : \"Failed to fetch GA4 channel mix\");\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -13,11 +13,12 @@ const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
13
13
  const ecommerceTrackingHealthSchema = z.object({
14
14
  startDate: z.string().regex(dateRegex).describe("Start date in YYYY-MM-DD format"),
15
15
  endDate: z.string().regex(dateRegex).describe("End date in YYYY-MM-DD format"),
16
- propertyId: z.string().optional().describe("GA4 property ID. If omitted, uses GA4_PROPERTY_ID")
16
+ propertyId: z.string().optional().describe("GA4 property ID. If omitted, resolves profileId mapping first and then GA4_PROPERTY_ID."),
17
+ profileId: z.string().optional().describe("Optional business profile ID used to resolve the mapped GA4 property.")
17
18
  });
18
19
  async function ecommerceTrackingHealthHandler(params) {
19
20
  try {
20
- const propertyId = resolveGa4PropertyId(params.propertyId);
21
+ const propertyId = await resolveGa4PropertyId(params.propertyId, params.profileId);
21
22
  const report = await runReport(propertyId, {
22
23
  dateRanges: [{ startDate: params.startDate, endDate: params.endDate }],
23
24
  metrics: [
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/tools/analytics/ecommerce-tracking-health.ts"],
4
- "sourcesContent": ["import { error, object } from \"mcp-use/server\";\nimport { z } from \"zod\";\n\nimport {\n extractQuotaSnapshot,\n getMetricValueByName,\n resolveGa4PropertyId,\n round,\n toPercent,\n} from \"../../analytics/ga4-report-utils.js\";\nimport { runReport } from \"../../services/analytics/ga4-client.js\";\nimport { stripNulls } from \"../../utils/strip-payload.js\";\n\nconst dateRegex = /^\\d{4}-\\d{2}-\\d{2}$/;\n\nexport const ecommerceTrackingHealthSchema = z.object({\n startDate: z.string().regex(dateRegex).describe(\"Start date in YYYY-MM-DD format\"),\n endDate: z.string().regex(dateRegex).describe(\"End date in YYYY-MM-DD format\"),\n propertyId: z.string().optional().describe(\"GA4 property ID. If omitted, uses GA4_PROPERTY_ID\"),\n});\n\nexport async function ecommerceTrackingHealthHandler(\n params: z.infer<typeof ecommerceTrackingHealthSchema>\n) {\n try {\n const propertyId = resolveGa4PropertyId(params.propertyId);\n const report = await runReport(propertyId, {\n dateRanges: [{ startDate: params.startDate, endDate: params.endDate }],\n metrics: [\n { name: \"itemViewEvents\" },\n { name: \"addToCarts\" },\n { name: \"checkouts\" },\n { name: \"transactions\" },\n { name: \"purchaseRevenue\" },\n { name: \"refundAmount\" },\n ],\n returnPropertyQuota: true,\n });\n\n const row = report.rows?.[0] ?? {};\n const itemViews = getMetricValueByName(row, report.metricHeaders, \"itemViewEvents\");\n const addToCarts = getMetricValueByName(row, report.metricHeaders, \"addToCarts\");\n const checkouts = getMetricValueByName(row, report.metricHeaders, \"checkouts\");\n const transactions = getMetricValueByName(row, report.metricHeaders, \"transactions\");\n const purchaseRevenue = getMetricValueByName(row, report.metricHeaders, \"purchaseRevenue\");\n const refundAmount = getMetricValueByName(row, report.metricHeaders, \"refundAmount\");\n\n const warnings: string[] = [];\n if (itemViews === 0) {\n warnings.push(\"No se detectaron view_item events en el rango consultado.\");\n }\n if (addToCarts === 0 && itemViews > 0) {\n warnings.push(\"Hay view_item pero no add_to_cart. Revisar tagging de add_to_cart.\");\n }\n if (checkouts === 0 && addToCarts > 0) {\n warnings.push(\"Hay add_to_cart pero no begin_checkout. Revisar continuidad del funnel.\");\n }\n if (transactions === 0 && checkouts > 0) {\n warnings.push(\"Hay begin_checkout pero no purchase/transactions. Revisar tagging de compra.\");\n }\n if (purchaseRevenue > 0 && transactions === 0) {\n warnings.push(\"Hay revenue pero no transactions. Revisar consistencia de ecommerce events.\");\n }\n\n return object(\n stripNulls({\n property_id: propertyId,\n date_range: {\n start_date: params.startDate,\n end_date: params.endDate,\n },\n currency_code: report.metadata?.currencyCode,\n funnel: {\n item_view_events: itemViews,\n add_to_carts: addToCarts,\n checkouts,\n transactions,\n purchase_revenue: round(purchaseRevenue),\n refund_amount: round(refundAmount),\n add_to_cart_rate_percent: round(toPercent(addToCarts, itemViews)),\n checkout_rate_percent: round(toPercent(checkouts, addToCarts)),\n purchase_rate_percent: round(toPercent(transactions, checkouts)),\n },\n warnings,\n quota: extractQuotaSnapshot(report),\n })\n );\n } catch (err) {\n return error(\n err instanceof Error ? err.message : \"Failed to evaluate GA4 ecommerce tracking health\"\n );\n }\n}\n"],
5
- "mappings": "AAAA,SAAS,OAAO,cAAc;AAC9B,SAAS,SAAS;AAElB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,iBAAiB;AAC1B,SAAS,kBAAkB;AAE3B,MAAM,YAAY;AAEX,MAAM,gCAAgC,EAAE,OAAO;AAAA,EACpD,WAAW,EAAE,OAAO,EAAE,MAAM,SAAS,EAAE,SAAS,iCAAiC;AAAA,EACjF,SAAS,EAAE,OAAO,EAAE,MAAM,SAAS,EAAE,SAAS,+BAA+B;AAAA,EAC7E,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,mDAAmD;AAChG,CAAC;AAED,eAAsB,+BACpB,QACA;AACA,MAAI;AACF,UAAM,aAAa,qBAAqB,OAAO,UAAU;AACzD,UAAM,SAAS,MAAM,UAAU,YAAY;AAAA,MACzC,YAAY,CAAC,EAAE,WAAW,OAAO,WAAW,SAAS,OAAO,QAAQ,CAAC;AAAA,MACrE,SAAS;AAAA,QACP,EAAE,MAAM,iBAAiB;AAAA,QACzB,EAAE,MAAM,aAAa;AAAA,QACrB,EAAE,MAAM,YAAY;AAAA,QACpB,EAAE,MAAM,eAAe;AAAA,QACvB,EAAE,MAAM,kBAAkB;AAAA,QAC1B,EAAE,MAAM,eAAe;AAAA,MACzB;AAAA,MACA,qBAAqB;AAAA,IACvB,CAAC;AAED,UAAM,MAAM,OAAO,OAAO,CAAC,KAAK,CAAC;AACjC,UAAM,YAAY,qBAAqB,KAAK,OAAO,eAAe,gBAAgB;AAClF,UAAM,aAAa,qBAAqB,KAAK,OAAO,eAAe,YAAY;AAC/E,UAAM,YAAY,qBAAqB,KAAK,OAAO,eAAe,WAAW;AAC7E,UAAM,eAAe,qBAAqB,KAAK,OAAO,eAAe,cAAc;AACnF,UAAM,kBAAkB,qBAAqB,KAAK,OAAO,eAAe,iBAAiB;AACzF,UAAM,eAAe,qBAAqB,KAAK,OAAO,eAAe,cAAc;AAEnF,UAAM,WAAqB,CAAC;AAC5B,QAAI,cAAc,GAAG;AACnB,eAAS,KAAK,2DAA2D;AAAA,IAC3E;AACA,QAAI,eAAe,KAAK,YAAY,GAAG;AACrC,eAAS,KAAK,oEAAoE;AAAA,IACpF;AACA,QAAI,cAAc,KAAK,aAAa,GAAG;AACrC,eAAS,KAAK,yEAAyE;AAAA,IACzF;AACA,QAAI,iBAAiB,KAAK,YAAY,GAAG;AACvC,eAAS,KAAK,8EAA8E;AAAA,IAC9F;AACA,QAAI,kBAAkB,KAAK,iBAAiB,GAAG;AAC7C,eAAS,KAAK,6EAA6E;AAAA,IAC7F;AAEA,WAAO;AAAA,MACL,WAAW;AAAA,QACT,aAAa;AAAA,QACb,YAAY;AAAA,UACV,YAAY,OAAO;AAAA,UACnB,UAAU,OAAO;AAAA,QACnB;AAAA,QACA,eAAe,OAAO,UAAU;AAAA,QAChC,QAAQ;AAAA,UACN,kBAAkB;AAAA,UAClB,cAAc;AAAA,UACd;AAAA,UACA;AAAA,UACA,kBAAkB,MAAM,eAAe;AAAA,UACvC,eAAe,MAAM,YAAY;AAAA,UACjC,0BAA0B,MAAM,UAAU,YAAY,SAAS,CAAC;AAAA,UAChE,uBAAuB,MAAM,UAAU,WAAW,UAAU,CAAC;AAAA,UAC7D,uBAAuB,MAAM,UAAU,cAAc,SAAS,CAAC;AAAA,QACjE;AAAA,QACA;AAAA,QACA,OAAO,qBAAqB,MAAM;AAAA,MACpC,CAAC;AAAA,IACH;AAAA,EACF,SAAS,KAAK;AACZ,WAAO;AAAA,MACL,eAAe,QAAQ,IAAI,UAAU;AAAA,IACvC;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import { error, object } from \"mcp-use/server\";\nimport { z } from \"zod\";\n\nimport {\n extractQuotaSnapshot,\n getMetricValueByName,\n resolveGa4PropertyId,\n round,\n toPercent,\n} from \"../../analytics/ga4-report-utils.js\";\nimport { runReport } from \"../../services/analytics/ga4-client.js\";\nimport { stripNulls } from \"../../utils/strip-payload.js\";\n\nconst dateRegex = /^\\d{4}-\\d{2}-\\d{2}$/;\n\nexport const ecommerceTrackingHealthSchema = z.object({\n startDate: z.string().regex(dateRegex).describe(\"Start date in YYYY-MM-DD format\"),\n endDate: z.string().regex(dateRegex).describe(\"End date in YYYY-MM-DD format\"),\n propertyId: z.string().optional().describe(\"GA4 property ID. If omitted, resolves profileId mapping first and then GA4_PROPERTY_ID.\"),\n profileId: z.string().optional().describe(\"Optional business profile ID used to resolve the mapped GA4 property.\"),\n});\n\nexport async function ecommerceTrackingHealthHandler(\n params: z.infer<typeof ecommerceTrackingHealthSchema>\n) {\n try {\n const propertyId = await resolveGa4PropertyId(params.propertyId, params.profileId);\n const report = await runReport(propertyId, {\n dateRanges: [{ startDate: params.startDate, endDate: params.endDate }],\n metrics: [\n { name: \"itemViewEvents\" },\n { name: \"addToCarts\" },\n { name: \"checkouts\" },\n { name: \"transactions\" },\n { name: \"purchaseRevenue\" },\n { name: \"refundAmount\" },\n ],\n returnPropertyQuota: true,\n });\n\n const row = report.rows?.[0] ?? {};\n const itemViews = getMetricValueByName(row, report.metricHeaders, \"itemViewEvents\");\n const addToCarts = getMetricValueByName(row, report.metricHeaders, \"addToCarts\");\n const checkouts = getMetricValueByName(row, report.metricHeaders, \"checkouts\");\n const transactions = getMetricValueByName(row, report.metricHeaders, \"transactions\");\n const purchaseRevenue = getMetricValueByName(row, report.metricHeaders, \"purchaseRevenue\");\n const refundAmount = getMetricValueByName(row, report.metricHeaders, \"refundAmount\");\n\n const warnings: string[] = [];\n if (itemViews === 0) {\n warnings.push(\"No se detectaron view_item events en el rango consultado.\");\n }\n if (addToCarts === 0 && itemViews > 0) {\n warnings.push(\"Hay view_item pero no add_to_cart. Revisar tagging de add_to_cart.\");\n }\n if (checkouts === 0 && addToCarts > 0) {\n warnings.push(\"Hay add_to_cart pero no begin_checkout. Revisar continuidad del funnel.\");\n }\n if (transactions === 0 && checkouts > 0) {\n warnings.push(\"Hay begin_checkout pero no purchase/transactions. Revisar tagging de compra.\");\n }\n if (purchaseRevenue > 0 && transactions === 0) {\n warnings.push(\"Hay revenue pero no transactions. Revisar consistencia de ecommerce events.\");\n }\n\n return object(\n stripNulls({\n property_id: propertyId,\n date_range: {\n start_date: params.startDate,\n end_date: params.endDate,\n },\n currency_code: report.metadata?.currencyCode,\n funnel: {\n item_view_events: itemViews,\n add_to_carts: addToCarts,\n checkouts,\n transactions,\n purchase_revenue: round(purchaseRevenue),\n refund_amount: round(refundAmount),\n add_to_cart_rate_percent: round(toPercent(addToCarts, itemViews)),\n checkout_rate_percent: round(toPercent(checkouts, addToCarts)),\n purchase_rate_percent: round(toPercent(transactions, checkouts)),\n },\n warnings,\n quota: extractQuotaSnapshot(report),\n })\n );\n } catch (err) {\n return error(\n err instanceof Error ? err.message : \"Failed to evaluate GA4 ecommerce tracking health\"\n );\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,OAAO,cAAc;AAC9B,SAAS,SAAS;AAElB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,iBAAiB;AAC1B,SAAS,kBAAkB;AAE3B,MAAM,YAAY;AAEX,MAAM,gCAAgC,EAAE,OAAO;AAAA,EACpD,WAAW,EAAE,OAAO,EAAE,MAAM,SAAS,EAAE,SAAS,iCAAiC;AAAA,EACjF,SAAS,EAAE,OAAO,EAAE,MAAM,SAAS,EAAE,SAAS,+BAA+B;AAAA,EAC7E,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,yFAAyF;AAAA,EACpI,WAAW,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,uEAAuE;AACnH,CAAC;AAED,eAAsB,+BACpB,QACA;AACA,MAAI;AACF,UAAM,aAAa,MAAM,qBAAqB,OAAO,YAAY,OAAO,SAAS;AACjF,UAAM,SAAS,MAAM,UAAU,YAAY;AAAA,MACzC,YAAY,CAAC,EAAE,WAAW,OAAO,WAAW,SAAS,OAAO,QAAQ,CAAC;AAAA,MACrE,SAAS;AAAA,QACP,EAAE,MAAM,iBAAiB;AAAA,QACzB,EAAE,MAAM,aAAa;AAAA,QACrB,EAAE,MAAM,YAAY;AAAA,QACpB,EAAE,MAAM,eAAe;AAAA,QACvB,EAAE,MAAM,kBAAkB;AAAA,QAC1B,EAAE,MAAM,eAAe;AAAA,MACzB;AAAA,MACA,qBAAqB;AAAA,IACvB,CAAC;AAED,UAAM,MAAM,OAAO,OAAO,CAAC,KAAK,CAAC;AACjC,UAAM,YAAY,qBAAqB,KAAK,OAAO,eAAe,gBAAgB;AAClF,UAAM,aAAa,qBAAqB,KAAK,OAAO,eAAe,YAAY;AAC/E,UAAM,YAAY,qBAAqB,KAAK,OAAO,eAAe,WAAW;AAC7E,UAAM,eAAe,qBAAqB,KAAK,OAAO,eAAe,cAAc;AACnF,UAAM,kBAAkB,qBAAqB,KAAK,OAAO,eAAe,iBAAiB;AACzF,UAAM,eAAe,qBAAqB,KAAK,OAAO,eAAe,cAAc;AAEnF,UAAM,WAAqB,CAAC;AAC5B,QAAI,cAAc,GAAG;AACnB,eAAS,KAAK,2DAA2D;AAAA,IAC3E;AACA,QAAI,eAAe,KAAK,YAAY,GAAG;AACrC,eAAS,KAAK,oEAAoE;AAAA,IACpF;AACA,QAAI,cAAc,KAAK,aAAa,GAAG;AACrC,eAAS,KAAK,yEAAyE;AAAA,IACzF;AACA,QAAI,iBAAiB,KAAK,YAAY,GAAG;AACvC,eAAS,KAAK,8EAA8E;AAAA,IAC9F;AACA,QAAI,kBAAkB,KAAK,iBAAiB,GAAG;AAC7C,eAAS,KAAK,6EAA6E;AAAA,IAC7F;AAEA,WAAO;AAAA,MACL,WAAW;AAAA,QACT,aAAa;AAAA,QACb,YAAY;AAAA,UACV,YAAY,OAAO;AAAA,UACnB,UAAU,OAAO;AAAA,QACnB;AAAA,QACA,eAAe,OAAO,UAAU;AAAA,QAChC,QAAQ;AAAA,UACN,kBAAkB;AAAA,UAClB,cAAc;AAAA,UACd;AAAA,UACA;AAAA,UACA,kBAAkB,MAAM,eAAe;AAAA,UACvC,eAAe,MAAM,YAAY;AAAA,UACjC,0BAA0B,MAAM,UAAU,YAAY,SAAS,CAAC;AAAA,UAChE,uBAAuB,MAAM,UAAU,WAAW,UAAU,CAAC;AAAA,UAC7D,uBAAuB,MAAM,UAAU,cAAc,SAAS,CAAC;AAAA,QACjE;AAAA,QACA;AAAA,QACA,OAAO,qBAAqB,MAAM;AAAA,MACpC,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
  }
@@ -12,11 +12,12 @@ const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
12
12
  const engagementOverviewSchema = z.object({
13
13
  startDate: z.string().regex(dateRegex).describe("Start date in YYYY-MM-DD format"),
14
14
  endDate: z.string().regex(dateRegex).describe("End date in YYYY-MM-DD format"),
15
- propertyId: z.string().optional().describe("GA4 property ID. If omitted, uses GA4_PROPERTY_ID")
15
+ propertyId: z.string().optional().describe("GA4 property ID. If omitted, resolves profileId mapping first and then GA4_PROPERTY_ID."),
16
+ profileId: z.string().optional().describe("Optional business profile ID used to resolve the mapped GA4 property.")
16
17
  });
17
18
  async function engagementOverviewHandler(params) {
18
19
  try {
19
- const propertyId = resolveGa4PropertyId(params.propertyId);
20
+ const propertyId = await resolveGa4PropertyId(params.propertyId, params.profileId);
20
21
  const report = await runReport(propertyId, {
21
22
  dateRanges: [{ startDate: params.startDate, endDate: params.endDate }],
22
23
  metrics: [