@getmcpads/google-search-console-mcp-server 1.0.2 → 2.0.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.
package/dist/index.js CHANGED
@@ -1,8 +1,290 @@
1
+ // src/parameter-descriptions.ts
2
+ var PARAMETER_DESCRIPTIONS = {
3
+ "filters": "Filters combined according to this tool's query schema. Use provider field names and the declared operators.",
4
+ "searchType": "Search surface to report, such as web, image, video, news or Discover. Availability depends on the property.",
5
+ "searchTypes": "Search surfaces to compare. Each surface is queried separately; do not sum overlapping surfaces.",
6
+ "datePreset": "Named reporting period. Use the custom date range when you need exact start and end dates.",
7
+ "dateRange": "Explicit reporting start and end dates in YYYY-MM-DD format.",
8
+ "timeRange": "Explicit provider reporting period with since and until dates.",
9
+ "startDate": "First date of the reporting period, in YYYY-MM-DD format.",
10
+ "endDate": "Last date of the reporting period, in YYYY-MM-DD format, on or after startDate.",
11
+ "since": "Beginning of the requested reporting period; use the format accepted by the provider.",
12
+ "until": "End of the requested reporting period; use the format accepted by the provider.",
13
+ "orderBy": "Field used to order report rows.",
14
+ "orderField": "Provider field used to order report rows.",
15
+ "orderDirection": "Sort direction for the selected ordering field.",
16
+ "orderType": "Sort direction for the selected ordering field.",
17
+ "sortBy": "Metric or dimension used to rank the returned results.",
18
+ "sort": "Native provider ordering expressions.",
19
+ "limit": "Maximum number of returned items or rows. The declared bounds and default apply; use pagination for additional results.",
20
+ "topN": "Maximum number of highest-ranked results to return.",
21
+ "pageSize": "Maximum items requested per page. Follow the returned pagination information for remaining results.",
22
+ "page": "Page number to request, starting at 1.",
23
+ "offset": "Zero-based number of rows to skip before returning results.",
24
+ "bookmark": "Opaque Pinterest pagination bookmark returned by the previous response. Omit on the first request.",
25
+ "after": "Opaque cursor for the next page returned by Meta.",
26
+ "before": "Opaque cursor for the previous page returned by Meta.",
27
+ "includeUnchanged": "Include entries with no observed change in the comparison.",
28
+ "minImpressions": "Minimum reported impressions required for a row to enter the analysis.",
29
+ "objectives": "Analysis goals that determine the recommended large-site sampling strategy.",
30
+ "pageLimit": "Maximum number of pages considered for the sampling plan.",
31
+ "maxInspectionUrls": "Maximum URL Inspection candidates included in the plan; inspection calls consume provider quota.",
32
+ "minImpressionsForLowCtr": "Minimum impressions before a page is considered for low-CTR analysis.",
33
+ "staleSitemapDays": "Number of days after which a sitemap's last download is considered stale.",
34
+ "continueOnError": "Continue with other URLs when one inspection fails, returning individual failures.",
35
+ "dimension": "Reporting dimension used to group and compare results.",
36
+ "maxClusters": "Maximum query clusters to return.",
37
+ "topQueriesPerCluster": "Maximum example queries returned for each cluster.",
38
+ "minPages": "Minimum distinct pages ranking for a query before it is considered a cannibalization candidate.",
39
+ "adAccountId": "Owning advertising account ID. Use the exact ID returned by account discovery; do not substitute a campaign or business ID.",
40
+ "advertiserId": "TikTok advertiser account ID as a string.",
41
+ "customerId": "Google Ads customer ID, without hyphens.",
42
+ "propertyId": "Google Analytics property ID; use the property, not the account or measurement ID.",
43
+ "level": "Entity or aggregation level for the requested report.",
44
+ "dataLevel": "TikTok reporting aggregation level. Metrics and dimensions must be compatible with this level.",
45
+ "granularity": "Time bucket used to aggregate reporting rows.",
46
+ "columns": "Native Pinterest reporting column names. Discover supported columns before selecting metrics.",
47
+ "executionMode": "Choose the supported synchronous, asynchronous or automatic report execution strategy.",
48
+ "entityIds": "Exact IDs of the entities to include in the selected reporting level.",
49
+ "targetingTypes": "Native targeting categories used to break down delivery.",
50
+ "clickWindowDays": "Click-through attribution window, in days.",
51
+ "engagementWindowDays": "Engagement-through attribution window, in days.",
52
+ "viewWindowDays": "View-through attribution window, in days.",
53
+ "conversionReportTime": "Whether conversions are assigned to the ad action date or the conversion date.",
54
+ "attributionTypes": "Provider attribution categories included in the report.",
55
+ "reportingTimezone": "Provider reporting timezone selector. Keep it consistent when comparing periods.",
56
+ "campaignIds": "Restrict results to these campaign IDs.",
57
+ "adGroupIds": "Restrict results to these ad group IDs.",
58
+ "adgroupIds": "Restrict TikTok results to these ad group IDs.",
59
+ "adIds": "Restrict results to these ad IDs.",
60
+ "campaignId": "Exact campaign ID in the selected advertising account.",
61
+ "adGroupId": "Exact ad group ID in the selected advertising account.",
62
+ "adSetId": "Exact Meta ad set ID in the selected advertising account.",
63
+ "adId": "Exact ad ID in the selected advertising account.",
64
+ "entityId": "Exact ID of the entity selected by this operation. It must belong to the selected account.",
65
+ "onlyWithPeriodDelivery": "Keep only creatives with observed delivery in the requested date range.",
66
+ "breakdown": "Native reporting breakdown used to group results.",
67
+ "breakdowns": "Native reporting breakdowns. Check compatibility before combining them.",
68
+ "productGroupIds": "Restrict reporting to these product group IDs.",
69
+ "productItemIds": "Restrict reporting to these product item IDs.",
70
+ "includeProductSamples": "Include a limited sample of product records alongside inventory diagnostics.",
71
+ "sampleProductGroups": "Maximum product groups to sample for inventory diagnostics.",
72
+ "reportName": "Provider report name identifying the requested report recipe.",
73
+ "conversionProductAttributionType": "Pinterest product-conversion attribution category.",
74
+ "conversionProductBreakdown": "Pinterest product-conversion grouping level.",
75
+ "productSkuIds": "Restrict product-conversion reporting to these SKU IDs.",
76
+ "request": "Exact request schema name to inspect before composing a write payload.",
77
+ "entity": "Entity collection or level to inspect.",
78
+ "entityType": "Provider entity type to retrieve.",
79
+ "entityStatuses": "Native status values used to filter the entity collection.",
80
+ "statusFilter": "Native status values used to filter the returned entities.",
81
+ "targetingType": "Targeting dictionary to inspect, such as interests, locations or demographics.",
82
+ "interestId": "Native interest ID for the targeting lookup.",
83
+ "mode": "Operation or analysis mode. Only the values declared in this schema are supported.",
84
+ "matchTypes": "Keyword match types included in the lookup.",
85
+ "countryCode": "Country code used to localize keyword or targeting results.",
86
+ "keywords": "Keyword phrases to analyze; use the language and market of the intended audience.",
87
+ "term": "Search term used for the keyword lookup.",
88
+ "terms": "Search terms used for the keyword lookup.",
89
+ "audienceId": "Exact ID of the audience to inspect or modify.",
90
+ "customerListId": "Exact customer-list ID. Reading metadata does not upload customer data.",
91
+ "businessId": "Exact business account ID used for the lookup.",
92
+ "accountType": "Provider account category used to filter results.",
93
+ "ownershipType": "Filter audiences by provider ownership category.",
94
+ "excludeNca": "Whether to exclude Pinterest new-customer-acquisition audiences from this lookup.",
95
+ "audienceInsightType": "Pinterest audience-insights category to request.",
96
+ "surfaces": "Conversion-setup surfaces to inspect, according to the supported values.",
97
+ "includeDeletedTags": "Include deleted conversion tags in diagnostics.",
98
+ "lookbackPeriod": "Provider lookback period used for conversion diagnostics.",
99
+ "sourcePlatform": "Filter conversion data by source platform.",
100
+ "ingestionSource": "Filter conversion events by ingestion source.",
101
+ "catalogId": "Exact product catalog ID accessible to the selected account.",
102
+ "feedId": "Exact catalog feed ID.",
103
+ "processingResultId": "Exact catalog feed processing result ID to inspect.",
104
+ "productGroupId": "Exact product group ID.",
105
+ "productSetId": "Exact Meta product set ID belonging to the selected catalog.",
106
+ "exportType": "Provider export category to request.",
107
+ "action": "Supported operation within this tool's scoped provider surface.",
108
+ "includeDetails": "Include detailed provider records in addition to summary information.",
109
+ "leadFormId": "Exact lead form ID to inspect.",
110
+ "subscriptionId": "Exact subscription ID to inspect.",
111
+ "assetId": "Exact provider asset ID.",
112
+ "memberId": "Exact business member ID used for the read lookup.",
113
+ "partnerId": "Exact business partner ID used for the read lookup.",
114
+ "pinId": "Exact Pinterest pin ID.",
115
+ "pinIds": "Exact Pinterest pin IDs to include.",
116
+ "metrics": "Native or documented calculated metric names to request. Check compatibility with the selected dimensions.",
117
+ "metric": "Metric used for the analysis; use a name from the platform metric catalogue.",
118
+ "boardId": "Exact Pinterest board ID.",
119
+ "searchTerm": "Text used to filter or search the selected collection.",
120
+ "region": "Provider region selector used to localize trends.",
121
+ "productRegion": "Region used for product trend analysis.",
122
+ "trendType": "Provider trend category to return.",
123
+ "interests": "Native interest categories used to filter trends.",
124
+ "genders": "Native gender categories used to filter aggregate trends.",
125
+ "ageBuckets": "Native age groups used to filter aggregate trends.",
126
+ "includeKeywords": "Include related keyword details in the trend response.",
127
+ "includeDemographics": "Include available aggregate demographic breakdowns.",
128
+ "productCategories": "Native product category IDs or values used to filter trends.",
129
+ "verticals": "Native business verticals used to filter trends.",
130
+ "productLookbackDays": "Product-trend lookback window, in days.",
131
+ "productEngagementType": "Engagement signal used for product-trend analysis.",
132
+ "featuredInterest": "Featured interest category for the trends lookup.",
133
+ "reportType": "Native provider report type; determines supported metrics and dimensions.",
134
+ "promotionId": "Exact Pinterest product-group promotion ID.",
135
+ "type": "Provider object or operation type selected from this schema's allowed values.",
136
+ "productBreakdown": "Native product grouping used to join catalog items with reported insights.",
137
+ "catalogProductLimit": "Maximum catalog product records read for the join.",
138
+ "includeAdsets": "Include ad set configuration when checking brand safety.",
139
+ "includeBlockLists": "Include accessible brand-safety block-list metadata.",
140
+ "includeRawTargeting": "Include the native targeting specification for diagnostics.",
141
+ "cellEntityType": "Entity type represented by experiment cells.",
142
+ "includePagePosts": "Read related Facebook Page posts when the token has permission.",
143
+ "includeInstagramMedia": "Read related Instagram media when the token has permission.",
144
+ "includeInsights": "Include available insight metrics; additional provider permissions may be required.",
145
+ "edge": "Allowlisted Meta Graph edge to read on the selected node.",
146
+ "fields": "Native provider fields to return. Select only fields supported by the chosen object or report.",
147
+ "filtering": "Native provider filter expressions; credentials and account overrides are not allowed.",
148
+ "includeSummary": "Request the provider's available summary or total-count metadata.",
149
+ "actionBreakdowns": "Meta action-level breakdowns, subject to Insights compatibility rules.",
150
+ "actionAttributionWindows": "Meta attribution windows used to attribute action metrics.",
151
+ "timeIncrement": "Provider time-bucket size or supported aggregate value.",
152
+ "locationTypes": "Location dictionary categories to include in targeting search.",
153
+ "adFormat": "Native Meta ad preview format; must be compatible with this ad's creative.",
154
+ "handle": "Asynchronous catalog batch handle returned by the submission to inspect.",
155
+ "keepEmptyRows": "Keep provider rows whose selected metrics are all zero, where supported.",
156
+ "sampleLimit": "Maximum records included in the diagnostic sample.",
157
+ "isOpenFunnel": "Allow users to enter at any funnel step instead of requiring the first step.",
158
+ "visualizationType": "Supported funnel visualization mode.",
159
+ "breakdownLimit": "Maximum breakdown values to include in the funnel response.",
160
+ "nextActionLimit": "Maximum next actions returned for each funnel step.",
161
+ "fallbackToStepCounts": "If the advanced funnel is unavailable, return independent step counts with the limitation made explicit.",
162
+ "report": "Native GA4 report request object for this endpoint.",
163
+ "reports": "Native GA4 report request objects to execute in one batch.",
164
+ "dimensions": "Native reporting dimensions. Validate compatibility with the selected metrics.",
165
+ "compatibilityFilter": "Limit compatibility results to the requested GA4 compatibility category.",
166
+ "collection": "Allowlisted administrative resource collection to read.",
167
+ "settings": "Property configuration surfaces to inspect.",
168
+ "includeRecurring": "Include recurring audience-export configurations.",
169
+ "audienceExportName": "Full GA4 audience-export resource name returned by list_audience_exports.",
170
+ "queryLifetime": "Request lifetime reporting instead of the explicit reporting period where TikTok supports it.",
171
+ "declineThresholdPct": "Percentage decline used as a candidate signal for creative fatigue, requiring further evidence.",
172
+ "includeSavedAudiences": "Include accessible saved-audience metadata in the overlap analysis.",
173
+ "overlapThreshold": "Overlap threshold used to flag audience pairs, in the units declared by this schema.",
174
+ "serviceType": "TikTok report service category.",
175
+ "catalog": "Allowlisted targeting dictionary or catalog to inspect.",
176
+ "query": "Search text used to find matching entries in the selected catalogue.",
177
+ "parameters": "Native query parameters for this allowlisted read endpoint. Do not provide credentials or account overrides.",
178
+ "account_id": "Exact account ID selected for this platform in the GetMCPAds workspace.",
179
+ "platform": "Platform key identifying the connected advertising or analytics source.",
180
+ "id": "Exact saved object ID returned by the corresponding create or list tool.",
181
+ "revision": "Last known object revision, used to reject conflicting updates. Read the object before editing.",
182
+ "name": "Human-readable name for this object.",
183
+ "title": "Human-readable title for the resulting object or review.",
184
+ "clientLabel": "Display label identifying the client in the saved view.",
185
+ "config": "Complete saved-view configuration, including source, selected account, period and display options.",
186
+ "subject": "Entity or creative subject whose dated performance will be analyzed.",
187
+ "compare_previous": "Include the immediately preceding period of equal duration for comparison.",
188
+ "profile": "Creative brief profile containing the client's business context and analysis preferences.",
189
+ "primaryMetric": "Metric used to rank or evaluate the review; it must be available for the selected platform.",
190
+ "direction": "Whether higher or lower values of the primary metric represent improvement.",
191
+ "creative_ids": "Exact creative IDs to include in this review.",
192
+ "request_id": "Caller-generated request identifier. Reuse it for a retry of the same operation to avoid duplicate work.",
193
+ "expiresInDays": "Number of days before the public review sharing link expires.",
194
+ "client_id": "Exact saved business-client workspace ID.",
195
+ "review_id": "Exact review ID returned by the review creation or listing tool.",
196
+ "page_id": "Exact page identifier within the landing-page review.",
197
+ "entity_id": "Exact entity identifier in the selected review or platform context.",
198
+ "source_key": "Source identifier returned by the business review; keep the platform/account association intact.",
199
+ "metric_key": "Exact metric key in the review evidence.",
200
+ "follow_up_on": "Follow-up date for the decision, using the format declared in this schema.",
201
+ "hypothesis": "Specific hypothesis to record, supported by the review evidence.",
202
+ "successCriterion": "Measurable condition used to decide whether the recorded action succeeded.",
203
+ "outcome": "Observed outcome to record; distinguish evidence from an untested expectation.",
204
+ "status": "Status selected from the allowed values for this object or operation.",
205
+ "retry_source": "Source identifier to retry after reviewing its reported error.",
206
+ "site_origin": "Website origin to analyze, including https:// and hostname, without credentials or a path.",
207
+ "matchType": "Google Ads keyword match type for the forecast.",
208
+ "negativeKeywords": "Keyword phrases excluded from the keyword forecast.",
209
+ "negativeMatchType": "Match type applied to the negative keywords.",
210
+ "geoTargetIds": "Google Ads geo-target constant IDs for the intended locations.",
211
+ "languageId": "Google Ads language constant ID for the keyword request.",
212
+ "languageIds": "Google Ads language constant IDs for the intended audience.",
213
+ "network": "Google Ads search network selector used for planning.",
214
+ "biddingStrategy": "Supported bidding strategy for this temporary forecast. The request does not create a live campaign.",
215
+ "includeAdultKeywords": "Allow adult keyword ideas where supported by the provider and the selected market.",
216
+ "seedKeywords": "Seed keyword phrases from which Google generates keyword ideas.",
217
+ "includeAverageCpc": "Request average cost-per-click metrics when available.",
218
+ "includeDeviceBreakdown": "Include device-specific planning metrics when supported.",
219
+ "includeKeywordConcepts": "Include Google's keyword concept grouping metadata.",
220
+ "startYearMonth": "First year and month of the historical metrics period.",
221
+ "endYearMonth": "Last year and month of the historical metrics period.",
222
+ "includeDismissed": "Include recommendations already dismissed in the account.",
223
+ "operation": "Exact allowlisted operation to execute. Arbitrary provider operations are not accepted.",
224
+ "category": "Category selected from the provider dictionary or field catalogue.",
225
+ "selectable": "Filter the field catalogue by whether the field can appear in SELECT.",
226
+ "filterable": "Filter the field catalogue by whether the field can appear in WHERE.",
227
+ "sortable": "Filter the field catalogue by whether the field can appear in ORDER BY.",
228
+ "pageToken": "Opaque continuation token returned by the preceding provider response.",
229
+ "locationNames": "Human-readable location names for Google to resolve into geo-target constants.",
230
+ "locale": "Locale used for human-readable lookup results.",
231
+ "resource": "Native Google Ads resource from which the GAQL query selects rows.",
232
+ "cursor": "Opaque pagination cursor returned by the preceding response; omit for the first page.",
233
+ "kind": "Entity or request kind whose schema or data is being requested.",
234
+ "startTime": "Beginning of the reporting period as an ISO 8601 timestamp with an explicit timezone.",
235
+ "endTime": "End of the reporting period as an ISO 8601 timestamp with an explicit timezone, after startTime.",
236
+ "reportDimension": "Native dimension used to break down the Snapchat report."
237
+ };
238
+
239
+ // src/tool-quality.ts
240
+ import { z } from "zod";
241
+ var RESULT_SCHEMA = {
242
+ type: "object",
243
+ properties: {
244
+ result: {
245
+ description: "Original tool result: parsed JSON when the text is JSON, otherwise the text or multiple MCP content blocks. Provider fields depend on the selected query.",
246
+ type: ["object", "array", "string", "number", "boolean", "null"]
247
+ }
248
+ },
249
+ required: ["result"],
250
+ additionalProperties: true
251
+ };
252
+ function toolAnnotations(write) {
253
+ return { readOnlyHint: !write, destructiveHint: write, idempotentHint: !write, openWorldHint: true };
254
+ }
255
+ function structuredResult(value) {
256
+ if (value.isError) return value;
257
+ let result = value.content;
258
+ const first = value.content[0];
259
+ if (value.content.length === 1 && first?.type === "text" && typeof first.text === "string") {
260
+ result = first.text;
261
+ try {
262
+ result = JSON.parse(first.text);
263
+ } catch {
264
+ }
265
+ }
266
+ return { ...value, structuredContent: { ...value.structuredContent, result } };
267
+ }
268
+ var resultShape = { result: z.union([z.object({}).passthrough(), z.array(z.unknown()), z.string(), z.number(), z.boolean(), z.null()]).describe(RESULT_SCHEMA.properties.result.description) };
269
+ function installToolQuality(server) {
270
+ server.tool = ((name, description, inputSchema, handler) => {
271
+ const write = Object.hasOwn(inputSchema, "confirm");
272
+ inputSchema = Object.fromEntries(Object.entries(inputSchema).map(([key, schema]) => [key, schema.description || PARAMETER_DESCRIPTIONS[key] ? schema.describe(schema.description || PARAMETER_DESCRIPTIONS[key]) : schema]));
273
+ return server.registerTool(name, {
274
+ title: name.replace(/_/g, " "),
275
+ description,
276
+ inputSchema,
277
+ outputSchema: resultShape,
278
+ annotations: toolAnnotations(write)
279
+ }, async (...args) => structuredResult(await handler(...args)));
280
+ });
281
+ }
282
+
1
283
  // src/server.ts
2
284
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
285
 
4
286
  // src/platforms/gsc/tools.ts
5
- import { z } from "zod";
287
+ import { z as z2 } from "zod";
6
288
 
7
289
  // src/core/errors.ts
8
290
  var PlatformApiError = class extends Error {
@@ -17,13 +299,6 @@ var PlatformApiError = class extends Error {
17
299
  this.retryAfter = retryAfter;
18
300
  this.name = "PlatformApiError";
19
301
  }
20
- platform;
21
- code;
22
- isRateLimit;
23
- isAuth;
24
- isPermission;
25
- suggestion;
26
- retryAfter;
27
302
  toMcpError() {
28
303
  return {
29
304
  error: this.message,
@@ -210,6 +485,33 @@ function flattenSearchAnalyticsResponse(response, dimensions) {
210
485
  }
211
486
 
212
487
  // src/platforms/gsc/gscClient.ts
488
+ function gscRouteClass(rawUrl) {
489
+ try {
490
+ const parsed = new URL(rawUrl);
491
+ if (parsed.pathname.endsWith("/searchAnalytics/query")) return "search_analytics";
492
+ if (parsed.pathname.endsWith("/urlInspection/index:inspect")) return "url_inspection";
493
+ if (parsed.pathname.includes("/sitemaps")) return "sitemaps";
494
+ if (parsed.pathname.includes("/sites")) return "sites";
495
+ } catch {
496
+ }
497
+ return "gsc_api";
498
+ }
499
+ function codeOwnedApiFailure(status) {
500
+ if (status === 401 || status === 403) {
501
+ return { message: "GSC API authorization failed.", status: "AUTHORIZATION_FAILED" };
502
+ }
503
+ if (status === 429) {
504
+ return { message: "GSC API rate limit was reached.", status: "RATE_LIMITED" };
505
+ }
506
+ if (status >= 500) {
507
+ return { message: "GSC API is temporarily unavailable.", status: "UPSTREAM_UNAVAILABLE" };
508
+ }
509
+ return { message: "GSC API rejected the request.", status: "REQUEST_REJECTED" };
510
+ }
511
+ var GSC_AUTO_PAGINATION_MAX_PAGES = 4;
512
+ var GSC_AUTO_PAGINATION_MAX_ROWS = 1e5;
513
+ var GSC_AUTO_PAGINATION_DEADLINE_MS = 3e4;
514
+ var GSC_SINGLE_REQUEST_TIMEOUT_MS = 9e4;
213
515
  function getAuthorizationUrl(clientId, redirectUri, state) {
214
516
  const params = new URLSearchParams({
215
517
  client_id: clientId,
@@ -283,16 +585,16 @@ var GSCClient = class {
283
585
  // ============================================
284
586
  // PRIVATE METHODS
285
587
  // ============================================
286
- async request(url, options = {}) {
588
+ async request(url, options = {}, timeoutMs = GSC_SINGLE_REQUEST_TIMEOUT_MS) {
589
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 1) {
590
+ throw new GSCApiException("GSC request deadline expired before dispatch", 408, "TIMEOUT");
591
+ }
287
592
  const controller = new AbortController();
288
- const timeout = setTimeout(() => controller.abort(), 9e4);
593
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
289
594
  try {
290
595
  const response = await fetch(url, {
291
596
  ...options,
292
597
  signal: controller.signal,
293
- // Forced after the spread: once a bearer token is attached, a redirect
294
- // must never be followed, or the credential would be forwarded to
295
- // whatever host the redirect names.
296
598
  redirect: "error",
297
599
  headers: {
298
600
  Authorization: `Bearer ${this.accessToken}`,
@@ -301,21 +603,22 @@ var GSCClient = class {
301
603
  }
302
604
  });
303
605
  if (!response.ok) {
304
- const errorBody = await response.json().catch(() => ({}));
305
- const errorDetails = errorBody?.error;
306
- console.error("[GSCClient] API Error:", response.status, JSON.stringify(errorBody, null, 2));
307
- const detailedMessage = errorDetails?.message || `Request failed: ${response.statusText}`;
606
+ const failure = codeOwnedApiFailure(response.status);
607
+ console.error("[GSCClient] API error", {
608
+ status: response.status,
609
+ route: gscRouteClass(url)
610
+ });
308
611
  throw new GSCApiException(
309
- detailedMessage,
612
+ failure.message,
310
613
  response.status,
311
- errorDetails?.status
614
+ failure.status
312
615
  );
313
616
  }
314
- return response.json();
617
+ return await response.json();
315
618
  } catch (error) {
316
619
  if (error instanceof DOMException && error.name === "AbortError") {
317
620
  throw new GSCApiException(
318
- "GSC API request timed out after 90 seconds",
621
+ "GSC API request exceeded its bounded deadline",
319
622
  408,
320
623
  "TIMEOUT"
321
624
  );
@@ -365,39 +668,80 @@ var GSCClient = class {
365
668
  const url = `${GSC_API_BASE}/webmasters/v3/sites/${encodedSiteUrl}/searchAnalytics/query`;
366
669
  const MAX_ROWS_PER_REQUEST = 25e3;
367
670
  const rowLimit = request.rowLimit ?? MAX_ROWS_PER_REQUEST;
368
- const initialOffset = request.startRow ?? 0;
671
+ const autoPaginate = request.rowLimit === void 0;
672
+ const paginationDeadline = Date.now() + GSC_AUTO_PAGINATION_DEADLINE_MS;
673
+ const remainingPaginationMs = () => {
674
+ const remaining = paginationDeadline - Date.now();
675
+ if (remaining < 1) {
676
+ throw new GSCApiException(
677
+ "GSC auto-pagination exceeded its 30-second total deadline",
678
+ 408,
679
+ "PAGINATION_DEADLINE"
680
+ );
681
+ }
682
+ return remaining;
683
+ };
369
684
  const firstResponse = await this.request(url, {
370
685
  method: "POST",
371
686
  body: JSON.stringify({
372
687
  ...request,
373
688
  rowLimit: Math.min(rowLimit, MAX_ROWS_PER_REQUEST),
374
- startRow: initialOffset
689
+ startRow: request.startRow ?? 0
375
690
  })
376
- });
691
+ }, autoPaginate ? remainingPaginationMs() : GSC_SINGLE_REQUEST_TIMEOUT_MS);
692
+ if (autoPaginate) remainingPaginationMs();
377
693
  const returnedRows = firstResponse.rows?.length ?? 0;
378
694
  if (request.rowLimit || returnedRows < MAX_ROWS_PER_REQUEST) {
379
- return firstResponse;
695
+ return {
696
+ ...firstResponse,
697
+ pagination: {
698
+ mode: autoPaginate ? "bounded_auto" : "single_page",
699
+ pageCount: 1,
700
+ rowCount: returnedRows,
701
+ maximumPages: autoPaginate ? GSC_AUTO_PAGINATION_MAX_PAGES : 1,
702
+ maximumRows: autoPaginate ? GSC_AUTO_PAGINATION_MAX_ROWS : rowLimit,
703
+ deadlineMs: autoPaginate ? GSC_AUTO_PAGINATION_DEADLINE_MS : GSC_SINGLE_REQUEST_TIMEOUT_MS,
704
+ truncated: false
705
+ }
706
+ };
380
707
  }
381
708
  const allRows = [...firstResponse.rows ?? []];
382
- let currentOffset = initialOffset + returnedRows;
383
- while (true) {
709
+ let currentOffset = (request.startRow ?? 0) + returnedRows;
710
+ let pageCount = 1;
711
+ let lastPageWasFull = returnedRows === MAX_ROWS_PER_REQUEST;
712
+ while (pageCount < GSC_AUTO_PAGINATION_MAX_PAGES && allRows.length < GSC_AUTO_PAGINATION_MAX_ROWS) {
713
+ const remainingRows = GSC_AUTO_PAGINATION_MAX_ROWS - allRows.length;
714
+ const requestedRows = Math.min(MAX_ROWS_PER_REQUEST, remainingRows);
384
715
  const pageResponse = await this.request(url, {
385
716
  method: "POST",
386
717
  body: JSON.stringify({
387
718
  ...request,
388
- rowLimit: MAX_ROWS_PER_REQUEST,
719
+ rowLimit: requestedRows,
389
720
  startRow: currentOffset
390
721
  })
391
- });
722
+ }, remainingPaginationMs());
723
+ remainingPaginationMs();
392
724
  const pageRows = pageResponse.rows?.length ?? 0;
725
+ pageCount += 1;
726
+ lastPageWasFull = pageRows === requestedRows;
393
727
  if (pageRows === 0) break;
394
- allRows.push(...pageResponse.rows ?? []);
728
+ allRows.push(...(pageResponse.rows ?? []).slice(0, remainingRows));
395
729
  currentOffset += pageRows;
396
- if (pageRows < MAX_ROWS_PER_REQUEST) break;
730
+ if (pageRows < requestedRows) break;
397
731
  }
732
+ remainingPaginationMs();
398
733
  return {
399
734
  ...firstResponse,
400
- rows: allRows
735
+ rows: allRows.slice(0, GSC_AUTO_PAGINATION_MAX_ROWS),
736
+ pagination: {
737
+ mode: "bounded_auto",
738
+ pageCount,
739
+ rowCount: Math.min(allRows.length, GSC_AUTO_PAGINATION_MAX_ROWS),
740
+ maximumPages: GSC_AUTO_PAGINATION_MAX_PAGES,
741
+ maximumRows: GSC_AUTO_PAGINATION_MAX_ROWS,
742
+ deadlineMs: GSC_AUTO_PAGINATION_DEADLINE_MS,
743
+ truncated: lastPageWasFull && (pageCount >= GSC_AUTO_PAGINATION_MAX_PAGES || allRows.length >= GSC_AUTO_PAGINATION_MAX_ROWS)
744
+ }
401
745
  };
402
746
  }
403
747
  // ============================================
@@ -838,36 +1182,37 @@ function planGSCQuery(params) {
838
1182
  }
839
1183
 
840
1184
  // src/platforms/gsc/tools.ts
841
- var siteUrlSchema = z.string().optional().describe("Search Console property URL, e.g. https://example.com/ or sc-domain:example.com. Uses GSC_SITE_URL when omitted.");
842
- var datePresetSchema = z.enum(["today", "yesterday", "last7days", "last28days", "last3months", "last6months", "last12months", "last16months"]);
843
- var searchTypeSchema = z.enum(["web", "image", "video", "news", "discover", "googleNews"]);
844
- var dataStateSchema = z.enum(["final", "all", "hourly_all"]);
845
- var aggregationTypeSchema = z.enum(["auto", "byPage", "byProperty", "byNewsShowcasePanel"]);
846
- var limitSchema = z.number().int().min(1).max(25e3).optional().default(1e3);
847
- var dateRangeSchema = z.object({
848
- startDate: z.string().describe("Start date YYYY-MM-DD"),
849
- endDate: z.string().describe("End date YYYY-MM-DD")
1185
+ var siteUrlSchema = z2.string().optional().describe("Search Console property URL, e.g. https://example.com/ or sc-domain:example.com. Uses GSC_SITE_URL when omitted.");
1186
+ var datePresetSchema = z2.enum(["today", "yesterday", "last7days", "last28days", "last3months", "last6months", "last12months", "last16months"]);
1187
+ var searchTypeSchema = z2.enum(["web", "image", "video", "news", "discover", "googleNews"]);
1188
+ var dataStateSchema = z2.enum(["final", "all", "hourly_all"]);
1189
+ var aggregationTypeSchema = z2.enum(["auto", "byPage", "byProperty", "byNewsShowcasePanel"]);
1190
+ var CompareSearchTypesSchema = z2.array(searchTypeSchema).min(1).max(6).refine((items) => new Set(items).size === items.length, "searchTypes must be unique");
1191
+ var limitSchema = z2.number().int().min(1).max(25e3).optional().default(1e3);
1192
+ var dateRangeSchema = z2.object({
1193
+ startDate: z2.string().describe("Start date YYYY-MM-DD"),
1194
+ endDate: z2.string().describe("End date YYYY-MM-DD")
850
1195
  });
851
- var comparisonDimensionSchema = z.enum(["query", "page"]);
852
- var sitemapBaselineSchema = z.object({
853
- path: z.string().describe("Sitemap URL/path returned by GSC."),
854
- submitted: z.number().int().min(0).optional().default(0),
855
- indexed: z.number().int().min(0).optional().default(0),
856
- warnings: z.number().int().min(0).optional().default(0),
857
- errors: z.number().int().min(0).optional().default(0),
858
- lastSubmitted: z.string().optional(),
859
- lastDownloaded: z.string().optional()
1196
+ var comparisonDimensionSchema = z2.enum(["query", "page"]);
1197
+ var sitemapBaselineSchema = z2.object({
1198
+ path: z2.string().describe("Sitemap URL/path returned by GSC."),
1199
+ submitted: z2.number().int().min(0).optional().default(0),
1200
+ indexed: z2.number().int().min(0).optional().default(0),
1201
+ warnings: z2.number().int().min(0).optional().default(0),
1202
+ errors: z2.number().int().min(0).optional().default(0),
1203
+ lastSubmitted: z2.string().optional(),
1204
+ lastDownloaded: z2.string().optional()
860
1205
  });
861
- var watchlistUrlSchema = z.object({
862
- url: z.string().url(),
863
- label: z.string().optional(),
864
- priority: z.enum(["low", "medium", "high", "critical"]).optional().default("medium"),
865
- tags: z.array(z.string()).optional().default([]),
866
- expectedVerdict: z.enum(["PASS", "PARTIAL", "FAIL", "NEUTRAL", "VERDICT_UNSPECIFIED"]).optional().default("PASS"),
867
- expectedCanonical: z.string().url().optional(),
868
- expectedCoverageState: z.string().optional()
1206
+ var watchlistUrlSchema = z2.object({
1207
+ url: z2.string().url(),
1208
+ label: z2.string().optional(),
1209
+ priority: z2.enum(["low", "medium", "high", "critical"]).optional().default("medium"),
1210
+ tags: z2.array(z2.string()).optional().default([]),
1211
+ expectedVerdict: z2.enum(["PASS", "PARTIAL", "FAIL", "NEUTRAL", "VERDICT_UNSPECIFIED"]).optional().default("PASS"),
1212
+ expectedCanonical: z2.string().url().optional(),
1213
+ expectedCoverageState: z2.string().optional()
869
1214
  });
870
- var samplingObjectiveSchema = z.enum(["topTraffic", "lowCtr", "declining", "sitemapRisk", "staleSitemaps"]);
1215
+ var samplingObjectiveSchema = z2.enum(["topTraffic", "lowCtr", "declining", "sitemapRisk", "staleSitemaps"]);
871
1216
  var BULK_URL_INSPECTION_LIMIT = 10;
872
1217
  var URL_INSPECTION_DAILY_QUOTA = 2e3;
873
1218
  var DEFAULT_FRESHNESS_LAG_WARNING_DAYS = 3;
@@ -998,7 +1343,6 @@ var AuthenticatedGSCClient = class {
998
1343
  this.tokenExpiresAt = Date.now() + 45 * 60 * 1e3;
999
1344
  }
1000
1345
  }
1001
- config;
1002
1346
  accessToken;
1003
1347
  tokenExpiresAt = 0;
1004
1348
  rateLimiter = new RateLimiter();
@@ -1528,26 +1872,26 @@ function registerGSCTools(server, config) {
1528
1872
  Use gsc://metrics, gsc://dimensions, gsc://filters, and gsc://compatibility for available fields and rules.`,
1529
1873
  {
1530
1874
  siteUrl: siteUrlSchema,
1531
- metrics: z.array(z.string()).optional().describe("Native: clicks, impressions, ctr, position. Calculated: ctrOpportunity, clicksPerImpression, impressionShare, clickShare."),
1532
- dimensions: z.array(z.string()).max(5).optional().describe("Up to 5 dimensions selected from query, page, country, device, date, hour, searchAppearance."),
1533
- filters: z.array(z.object({
1534
- field: z.string(),
1535
- operator: z.enum(["contains", "equals", "notContains", "notEquals", "includingRegex", "excludingRegex"]),
1536
- value: z.union([z.string(), z.number(), z.boolean()])
1875
+ metrics: z2.array(z2.string()).optional().describe("Native: clicks, impressions, ctr, position. Calculated: ctrOpportunity, clicksPerImpression, impressionShare, clickShare."),
1876
+ dimensions: z2.array(z2.string()).max(5).optional().describe("Up to 5 dimensions selected from query, page, country, device, date, hour, searchAppearance."),
1877
+ filters: z2.array(z2.object({
1878
+ field: z2.string(),
1879
+ operator: z2.enum(["contains", "equals", "notContains", "notEquals", "includingRegex", "excludingRegex"]),
1880
+ value: z2.union([z2.string(), z2.number(), z2.boolean()])
1537
1881
  })).optional(),
1538
1882
  searchType: searchTypeSchema.optional().default("web"),
1539
1883
  dataState: dataStateSchema.optional().default("all").describe("final=stable data, all=fresh daily data, hourly_all=fresh hourly data (use hour dimension)."),
1540
1884
  aggregationType: aggregationTypeSchema.optional().default("auto").describe("Official GSC aggregation. byProperty is incompatible with page grouping/filtering and Discover/Google News. byNewsShowcasePanel requires discover/googleNews plus searchAppearance equals NEWS_SHOWCASE, and forbids page grouping/filtering or another searchAppearance filter."),
1541
1885
  datePreset: datePresetSchema.optional().default("last28days"),
1542
- dateRange: z.object({
1543
- startDate: z.string().describe("Start date YYYY-MM-DD"),
1544
- endDate: z.string().describe("End date YYYY-MM-DD")
1886
+ dateRange: z2.object({
1887
+ startDate: z2.string().describe("Start date YYYY-MM-DD"),
1888
+ endDate: z2.string().describe("End date YYYY-MM-DD")
1545
1889
  }).optional(),
1546
- orderBy: z.enum(["clicks", "impressions", "ctr", "position", "ctrOpportunity", "clicksPerImpression", "impressionShare", "clickShare"]).optional().default("clicks"),
1547
- orderDirection: z.enum(["ASC", "DESC"]).optional().default("DESC"),
1890
+ orderBy: z2.enum(["clicks", "impressions", "ctr", "position", "ctrOpportunity", "clicksPerImpression", "impressionShare", "clickShare"]).optional().default("clicks"),
1891
+ orderDirection: z2.enum(["ASC", "DESC"]).optional().default("DESC"),
1548
1892
  limit: limitSchema,
1549
- startRow: z.number().int().min(0).optional().describe("Optional 0-based row offset for explicit pagination."),
1550
- autoPaginate: z.boolean().optional().default(false).describe("When true, omit rowLimit so the GSC client auto-paginates until exhaustion.")
1893
+ startRow: z2.number().int().min(0).optional().describe("Optional 0-based row offset for explicit pagination."),
1894
+ autoPaginate: z2.boolean().optional().default(false).describe("When true, omit rowLimit so the GSC client auto-paginates until exhaustion.")
1551
1895
  },
1552
1896
  async ({ siteUrl, metrics, dimensions, filters, searchType, dataState, aggregationType, datePreset, dateRange, orderBy, orderDirection, limit, startRow, autoPaginate }) => {
1553
1897
  try {
@@ -1597,6 +1941,11 @@ Use gsc://metrics, gsc://dimensions, gsc://filters, and gsc://compatibility for
1597
1941
  mode: autoPaginate ? "auto_startRow" : "single_page",
1598
1942
  autoPaginate,
1599
1943
  startRow: startRow ?? 0,
1944
+ pageCount: response.pagination?.pageCount ?? 1,
1945
+ maximumPages: response.pagination?.maximumPages ?? 1,
1946
+ maximumRows: response.pagination?.maximumRows ?? limit,
1947
+ deadlineMs: response.pagination?.deadlineMs ?? 9e4,
1948
+ truncated: response.pagination?.truncated ?? false,
1600
1949
  limit: autoPaginate ? null : limit
1601
1950
  },
1602
1951
  dataState,
@@ -1613,8 +1962,8 @@ Use gsc://metrics, gsc://dimensions, gsc://filters, and gsc://compatibility for
1613
1962
  "Inspect a URL for Google indexing status, crawl info, mobile usability, AMP, and rich results. Quota: 2000 requests/day/property.",
1614
1963
  {
1615
1964
  siteUrl: siteUrlSchema,
1616
- url: z.string().url().describe("Full URL to inspect."),
1617
- languageCode: z.string().optional().describe("Optional IETF language tag, e.g. en-US or fr-FR.")
1965
+ url: z2.string().url().describe("Full URL to inspect."),
1966
+ languageCode: z2.string().optional().describe("Optional IETF language tag, e.g. en-US or fr-FR.")
1618
1967
  },
1619
1968
  async ({ siteUrl, url, languageCode }) => {
1620
1969
  try {
@@ -1639,9 +1988,9 @@ Use gsc://metrics, gsc://dimensions, gsc://filters, and gsc://compatibility for
1639
1988
  "Inspect multiple URLs with a conservative sequential limit and normalized URL Inspection results. Quota: 2000 requests/day/property.",
1640
1989
  {
1641
1990
  siteUrl: siteUrlSchema,
1642
- urls: z.array(z.string().url()).min(1).max(BULK_URL_INSPECTION_LIMIT).describe(`Full URLs to inspect. Maximum ${BULK_URL_INSPECTION_LIMIT} per call to protect URL Inspection quota.`),
1643
- languageCode: z.string().optional().describe("Optional IETF language tag, e.g. en-US or fr-FR."),
1644
- continueOnError: z.boolean().optional().default(true).describe("When true, returns per-URL errors instead of failing the entire batch.")
1991
+ urls: z2.array(z2.string().url()).min(1).max(BULK_URL_INSPECTION_LIMIT).describe(`Full URLs to inspect. Maximum ${BULK_URL_INSPECTION_LIMIT} per call to protect URL Inspection quota.`),
1992
+ languageCode: z2.string().optional().describe("Optional IETF language tag, e.g. en-US or fr-FR."),
1993
+ continueOnError: z2.boolean().optional().default(true).describe("When true, returns per-URL errors instead of failing the entire batch.")
1645
1994
  },
1646
1995
  async ({ siteUrl, urls, languageCode, continueOnError }) => {
1647
1996
  try {
@@ -1681,7 +2030,7 @@ Use gsc://metrics, gsc://dimensions, gsc://filters, and gsc://compatibility for
1681
2030
  "List sitemaps submitted for a Search Console property, including processing status and submitted/indexed counts.",
1682
2031
  {
1683
2032
  siteUrl: siteUrlSchema,
1684
- sitemapIndex: z.string().url().optional().describe("Optional sitemap index URL used to return only its child sitemaps.")
2033
+ sitemapIndex: z2.string().url().optional().describe("Optional sitemap index URL used to return only its child sitemaps.")
1685
2034
  },
1686
2035
  async ({ siteUrl, sitemapIndex }) => {
1687
2036
  try {
@@ -1709,7 +2058,7 @@ Use gsc://metrics, gsc://dimensions, gsc://filters, and gsc://compatibility for
1709
2058
  "Get one submitted sitemap by full feed path, including type, fetch state, warnings/errors, and submitted/indexed content totals.",
1710
2059
  {
1711
2060
  siteUrl: siteUrlSchema,
1712
- feedpath: z.string().url().describe("Full sitemap URL exactly as submitted to Search Console.")
2061
+ feedpath: z2.string().url().describe("Full sitemap URL exactly as submitted to Search Console.")
1713
2062
  },
1714
2063
  async ({ siteUrl, feedpath }) => {
1715
2064
  try {
@@ -1786,9 +2135,9 @@ Use gsc://metrics, gsc://dimensions, gsc://filters, and gsc://compatibility for
1786
2135
  "Compare performance across Search Console search types: web, image, video, news, discover, and Google News.",
1787
2136
  {
1788
2137
  siteUrl: siteUrlSchema,
1789
- searchTypes: z.array(searchTypeSchema).optional().default(["web", "image", "video", "discover"]),
2138
+ searchTypes: CompareSearchTypesSchema.optional().default(["web", "image", "video", "discover"]),
1790
2139
  datePreset: datePresetSchema.optional().default("last28days"),
1791
- dateRange: z.object({ startDate: z.string(), endDate: z.string() }).optional()
2140
+ dateRange: z2.object({ startDate: z2.string(), endDate: z2.string() }).optional()
1792
2141
  },
1793
2142
  async ({ siteUrl, searchTypes, datePreset, dateRange }) => {
1794
2143
  try {
@@ -1839,7 +2188,7 @@ Use gsc://metrics, gsc://dimensions, gsc://filters, and gsc://compatibility for
1839
2188
  {
1840
2189
  siteUrl: siteUrlSchema,
1841
2190
  searchType: searchTypeSchema.optional().default("web"),
1842
- lookbackDays: z.number().int().min(3).max(30).optional().default(14).describe("Recent days to scan for daily data. Max 30.")
2191
+ lookbackDays: z2.number().int().min(3).max(30).optional().default(14).describe("Recent days to scan for daily data. Max 30.")
1843
2192
  },
1844
2193
  async ({ siteUrl, searchType, lookbackDays }) => {
1845
2194
  try {
@@ -1897,11 +2246,11 @@ Use gsc://metrics, gsc://dimensions, gsc://filters, and gsc://compatibility for
1897
2246
  "Read-only monitoring snapshot for Search Analytics freshness, sitemap indexation totals, and an optional small URL Inspection sample.",
1898
2247
  {
1899
2248
  siteUrl: siteUrlSchema,
1900
- searchTypes: z.array(searchTypeSchema).min(1).max(6).optional().default(["web"]),
1901
- lookbackDays: z.number().int().min(3).max(30).optional().default(14).describe("Recent days to scan for Search Analytics freshness."),
1902
- freshnessLagWarningDays: z.number().int().min(1).max(14).optional().default(DEFAULT_FRESHNESS_LAG_WARNING_DAYS).describe("Maximum acceptable lag in days before freshness is marked delayed/stale."),
1903
- inspectUrls: z.array(z.string().url()).max(BULK_URL_INSPECTION_LIMIT).optional().default([]).describe(`Optional URLs to inspect. Maximum ${BULK_URL_INSPECTION_LIMIT} to protect URL Inspection quota.`),
1904
- languageCode: z.string().optional().describe("Optional IETF language tag for URL Inspection, e.g. en-US or fr-FR.")
2249
+ searchTypes: z2.array(searchTypeSchema).min(1).max(6).optional().default(["web"]),
2250
+ lookbackDays: z2.number().int().min(3).max(30).optional().default(14).describe("Recent days to scan for Search Analytics freshness."),
2251
+ freshnessLagWarningDays: z2.number().int().min(1).max(14).optional().default(DEFAULT_FRESHNESS_LAG_WARNING_DAYS).describe("Maximum acceptable lag in days before freshness is marked delayed/stale."),
2252
+ inspectUrls: z2.array(z2.string().url()).max(BULK_URL_INSPECTION_LIMIT).optional().default([]).describe(`Optional URLs to inspect. Maximum ${BULK_URL_INSPECTION_LIMIT} to protect URL Inspection quota.`),
2253
+ languageCode: z2.string().optional().describe("Optional IETF language tag for URL Inspection, e.g. en-US or fr-FR.")
1905
2254
  },
1906
2255
  async ({ siteUrl, searchTypes, lookbackDays, freshnessLagWarningDays, inspectUrls, languageCode }) => {
1907
2256
  try {
@@ -2004,9 +2353,9 @@ Use gsc://metrics, gsc://dimensions, gsc://filters, and gsc://compatibility for
2004
2353
  "Read-only sitemap delta tracker. Fetches the current sitemap snapshot and compares it to an agent-provided baseline; no state is written.",
2005
2354
  {
2006
2355
  siteUrl: siteUrlSchema,
2007
- baseline: z.array(sitemapBaselineSchema).optional().default([]).describe("Previous snapshot entries from this tool's snapshot output."),
2008
- includeUnchanged: z.boolean().optional().default(false),
2009
- minAbsIndexedDelta: z.number().int().min(0).optional().default(1).describe("Minimum absolute indexed URL delta to include otherwise unchanged sitemaps.")
2356
+ baseline: z2.array(sitemapBaselineSchema).optional().default([]).describe("Previous snapshot entries from this tool's snapshot output."),
2357
+ includeUnchanged: z2.boolean().optional().default(false),
2358
+ minAbsIndexedDelta: z2.number().int().min(0).optional().default(1).describe("Minimum absolute indexed URL delta to include otherwise unchanged sitemaps.")
2010
2359
  },
2011
2360
  async ({ siteUrl, baseline, includeUnchanged, minAbsIndexedDelta }) => {
2012
2361
  try {
@@ -2101,11 +2450,11 @@ Use gsc://metrics, gsc://dimensions, gsc://filters, and gsc://compatibility for
2101
2450
  datePreset: datePresetSchema.optional().default("last28days"),
2102
2451
  currentDateRange: dateRangeSchema.optional().describe("Current period. Defaults to datePreset."),
2103
2452
  previousDateRange: dateRangeSchema.optional().describe("Previous period. Defaults to the immediately preceding period with the same length."),
2104
- includePages: z.boolean().optional().default(false).describe("When true, groups by searchAppearance + page for page-level rich result trends."),
2105
- limit: z.number().int().min(1).max(25e3).optional().default(5e3),
2106
- topN: z.number().int().min(1).max(100).optional().default(50),
2107
- minImpressions: z.number().int().min(0).optional().default(0),
2108
- sortBy: z.enum(["currentClicks", "currentImpressions", "clicksDelta", "impressionsDelta", "positionDelta"]).optional().default("impressionsDelta")
2453
+ includePages: z2.boolean().optional().default(false).describe("When true, groups by searchAppearance + page for page-level rich result trends."),
2454
+ limit: z2.number().int().min(1).max(25e3).optional().default(5e3),
2455
+ topN: z2.number().int().min(1).max(100).optional().default(50),
2456
+ minImpressions: z2.number().int().min(0).optional().default(0),
2457
+ sortBy: z2.enum(["currentClicks", "currentImpressions", "clicksDelta", "impressionsDelta", "positionDelta"]).optional().default("impressionsDelta")
2109
2458
  },
2110
2459
  async ({ siteUrl, searchType, datePreset, currentDateRange, previousDateRange, includePages, limit, topN, minImpressions, sortBy }) => {
2111
2460
  try {
@@ -2215,11 +2564,11 @@ Use gsc://metrics, gsc://dimensions, gsc://filters, and gsc://compatibility for
2215
2564
  searchType: searchTypeSchema.optional().default("web"),
2216
2565
  datePreset: datePresetSchema.optional().default("last28days"),
2217
2566
  dateRange: dateRangeSchema.optional(),
2218
- objectives: z.array(samplingObjectiveSchema).min(1).max(5).optional().default(["topTraffic", "lowCtr", "declining", "sitemapRisk", "staleSitemaps"]),
2219
- pageLimit: z.number().int().min(1).max(25e3).optional().default(5e3),
2220
- maxInspectionUrls: z.number().int().min(1).max(BULK_URL_INSPECTION_LIMIT).optional().default(BULK_URL_INSPECTION_LIMIT),
2221
- minImpressionsForLowCtr: z.number().int().min(1).optional().default(100),
2222
- staleSitemapDays: z.number().int().min(1).max(180).optional().default(14)
2567
+ objectives: z2.array(samplingObjectiveSchema).min(1).max(5).optional().default(["topTraffic", "lowCtr", "declining", "sitemapRisk", "staleSitemaps"]),
2568
+ pageLimit: z2.number().int().min(1).max(25e3).optional().default(5e3),
2569
+ maxInspectionUrls: z2.number().int().min(1).max(BULK_URL_INSPECTION_LIMIT).optional().default(BULK_URL_INSPECTION_LIMIT),
2570
+ minImpressionsForLowCtr: z2.number().int().min(1).optional().default(100),
2571
+ staleSitemapDays: z2.number().int().min(1).max(180).optional().default(14)
2223
2572
  },
2224
2573
  async ({ siteUrl, searchType, datePreset, dateRange, objectives, pageLimit, maxInspectionUrls, minImpressionsForLowCtr, staleSitemapDays }) => {
2225
2574
  try {
@@ -2348,9 +2697,9 @@ Use gsc://metrics, gsc://dimensions, gsc://filters, and gsc://compatibility for
2348
2697
  "Read-only URL Inspection watchlist for critical URLs. Inspects up to 10 URLs and returns alert/review/ok assessments without storing state.",
2349
2698
  {
2350
2699
  siteUrl: siteUrlSchema,
2351
- urls: z.array(watchlistUrlSchema).min(1).max(BULK_URL_INSPECTION_LIMIT).describe(`Watchlist entries. Maximum ${BULK_URL_INSPECTION_LIMIT} per call to protect URL Inspection quota.`),
2352
- languageCode: z.string().optional().describe("Optional IETF language tag, e.g. en-US or fr-FR."),
2353
- continueOnError: z.boolean().optional().default(true)
2700
+ urls: z2.array(watchlistUrlSchema).min(1).max(BULK_URL_INSPECTION_LIMIT).describe(`Watchlist entries. Maximum ${BULK_URL_INSPECTION_LIMIT} per call to protect URL Inspection quota.`),
2701
+ languageCode: z2.string().optional().describe("Optional IETF language tag, e.g. en-US or fr-FR."),
2702
+ continueOnError: z2.boolean().optional().default(true)
2354
2703
  },
2355
2704
  async ({ siteUrl, urls, languageCode, continueOnError }) => {
2356
2705
  try {
@@ -2417,10 +2766,10 @@ Use gsc://metrics, gsc://dimensions, gsc://filters, and gsc://compatibility for
2417
2766
  datePreset: datePresetSchema.optional().default("last28days"),
2418
2767
  currentDateRange: dateRangeSchema.optional().describe("Current period. Defaults to datePreset."),
2419
2768
  previousDateRange: dateRangeSchema.optional().describe("Previous period. Defaults to the immediately preceding period with the same length."),
2420
- limit: z.number().int().min(1).max(25e3).optional().default(5e3).describe("Rows to fetch for each period."),
2421
- topN: z.number().int().min(1).max(100).optional().default(25),
2422
- minAbsClickDelta: z.number().min(0).optional().default(0).describe("Filter out rows with smaller absolute click delta."),
2423
- sortBy: z.enum(["clicksDelta", "impressionsDelta", "ctrDelta", "positionDelta"]).optional().default("clicksDelta")
2769
+ limit: z2.number().int().min(1).max(25e3).optional().default(5e3).describe("Rows to fetch for each period."),
2770
+ topN: z2.number().int().min(1).max(100).optional().default(25),
2771
+ minAbsClickDelta: z2.number().min(0).optional().default(0).describe("Filter out rows with smaller absolute click delta."),
2772
+ sortBy: z2.enum(["clicksDelta", "impressionsDelta", "ctrDelta", "positionDelta"]).optional().default("clicksDelta")
2424
2773
  },
2425
2774
  async ({ siteUrl, dimension, searchType, datePreset, currentDateRange, previousDateRange, limit, topN, minAbsClickDelta, sortBy }) => {
2426
2775
  try {
@@ -2536,11 +2885,11 @@ Use gsc://metrics, gsc://dimensions, gsc://filters, and gsc://compatibility for
2536
2885
  searchType: searchTypeSchema.optional().default("web"),
2537
2886
  datePreset: datePresetSchema.optional().default("last28days"),
2538
2887
  dateRange: dateRangeSchema.optional(),
2539
- brandTerms: z.array(z.string()).optional().default([]).describe("Optional brand terms. Defaults to terms derived from the property domain."),
2540
- categoryRules: z.record(z.array(z.string())).optional().describe("Optional map of category name to matching tokens/phrases."),
2541
- limit: z.number().int().min(1).max(25e3).optional().default(5e3),
2542
- maxClusters: z.number().int().min(1).max(100).optional().default(25),
2543
- topQueriesPerCluster: z.number().int().min(1).max(25).optional().default(10)
2888
+ brandTerms: z2.array(z2.string()).optional().default([]).describe("Optional brand terms. Defaults to terms derived from the property domain."),
2889
+ categoryRules: z2.record(z2.array(z2.string())).optional().describe("Optional map of category name to matching tokens/phrases."),
2890
+ limit: z2.number().int().min(1).max(25e3).optional().default(5e3),
2891
+ maxClusters: z2.number().int().min(1).max(100).optional().default(25),
2892
+ topQueriesPerCluster: z2.number().int().min(1).max(25).optional().default(10)
2544
2893
  },
2545
2894
  async ({ siteUrl, searchType, datePreset, dateRange, brandTerms, categoryRules, limit, maxClusters, topQueriesPerCluster }) => {
2546
2895
  try {
@@ -2617,11 +2966,11 @@ Use gsc://metrics, gsc://dimensions, gsc://filters, and gsc://compatibility for
2617
2966
  searchType: searchTypeSchema.optional().default("web"),
2618
2967
  datePreset: datePresetSchema.optional().default("last28days"),
2619
2968
  dateRange: dateRangeSchema.optional(),
2620
- limit: z.number().int().min(1).max(25e3).optional().default(5e3),
2621
- minPages: z.number().int().min(2).max(10).optional().default(2),
2622
- minImpressions: z.number().int().min(0).optional().default(100),
2623
- maxTopPageClickShare: z.number().min(0.1).max(0.99).optional().default(0.8).describe("Only flag queries where the top page owns less than or equal to this click share."),
2624
- topN: z.number().int().min(1).max(100).optional().default(25)
2969
+ limit: z2.number().int().min(1).max(25e3).optional().default(5e3),
2970
+ minPages: z2.number().int().min(2).max(10).optional().default(2),
2971
+ minImpressions: z2.number().int().min(0).optional().default(100),
2972
+ maxTopPageClickShare: z2.number().min(0.1).max(0.99).optional().default(0.8).describe("Only flag queries where the top page owns less than or equal to this click share."),
2973
+ topN: z2.number().int().min(1).max(100).optional().default(25)
2625
2974
  },
2626
2975
  async ({ siteUrl, searchType, datePreset, dateRange, limit, minPages, minImpressions, maxTopPageClickShare, topN }) => {
2627
2976
  try {
@@ -2717,9 +3066,9 @@ Use gsc://metrics, gsc://dimensions, gsc://filters, and gsc://compatibility for
2717
3066
  "gsc_validate_query",
2718
3067
  "Validate a Search Console metric/dimension/search type combination before executing it.",
2719
3068
  {
2720
- dimensions: z.array(z.string()).optional().describe("Dimensions to validate."),
3069
+ dimensions: z2.array(z2.string()).optional().describe("Dimensions to validate."),
2721
3070
  searchType: searchTypeSchema.optional().default("web"),
2722
- dateRange: z.object({ startDate: z.string(), endDate: z.string() }).optional()
3071
+ dateRange: z2.object({ startDate: z2.string(), endDate: z2.string() }).optional()
2723
3072
  },
2724
3073
  async ({ dimensions, searchType, dateRange }) => {
2725
3074
  try {
@@ -3002,26 +3351,27 @@ function registerGSC(server, config) {
3002
3351
  }
3003
3352
 
3004
3353
  // src/server.ts
3005
- var PACKAGE_VERSION = "1.0.2";
3354
+ var PACKAGE_VERSION = "2.0.0";
3006
3355
  function createServer(config) {
3007
3356
  const server = new McpServer(
3008
- { name: "google-search-console-mcp", version: PACKAGE_VERSION },
3357
+ { name: "google-search-console-mcp", version: PACKAGE_VERSION, title: "Google Search Console", websiteUrl: "https://www.getmcpads.com/tools/search-console", icons: [{ src: "https://mcp.getmcpads.com/icon.svg", mimeType: "image/svg+xml" }] },
3009
3358
  { capabilities: { tools: { listChanged: true }, resources: { subscribe: false, listChanged: true } } }
3010
3359
  );
3360
+ installToolQuality(server);
3011
3361
  registerGSC(server, config);
3012
3362
  logger.system(`google-search-console-mcp v${PACKAGE_VERSION} ready, read-only`);
3013
3363
  return server;
3014
3364
  }
3015
3365
 
3016
3366
  // src/config.ts
3017
- import { z as z2 } from "zod";
3018
- var configSchema = z2.object({
3019
- clientId: z2.string().optional(),
3020
- clientSecret: z2.string().optional(),
3021
- refreshToken: z2.string().optional(),
3022
- accessToken: z2.string().optional(),
3023
- defaultSiteUrl: z2.string().optional(),
3024
- logLevel: z2.enum(["debug", "info", "warn", "error"]).default("info")
3367
+ import { z as z3 } from "zod";
3368
+ var configSchema = z3.object({
3369
+ clientId: z3.string().optional(),
3370
+ clientSecret: z3.string().optional(),
3371
+ refreshToken: z3.string().optional(),
3372
+ accessToken: z3.string().optional(),
3373
+ defaultSiteUrl: z3.string().optional(),
3374
+ logLevel: z3.enum(["debug", "info", "warn", "error"]).default("info")
3025
3375
  });
3026
3376
  function loadConfig() {
3027
3377
  const raw = {