@getmcpads/google-search-console-mcp-server 1.0.2 → 2.0.1

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/cli.js CHANGED
@@ -64,11 +64,293 @@ function loadConfig() {
64
64
  return config;
65
65
  }
66
66
 
67
+ // src/parameter-descriptions.ts
68
+ var PARAMETER_DESCRIPTIONS = {
69
+ "filters": "Filters combined according to this tool's query schema. Use provider field names and the declared operators.",
70
+ "searchType": "Search surface to report, such as web, image, video, news or Discover. Availability depends on the property.",
71
+ "searchTypes": "Search surfaces to compare. Each surface is queried separately; do not sum overlapping surfaces.",
72
+ "datePreset": "Named reporting period. Use the custom date range when you need exact start and end dates.",
73
+ "dateRange": "Explicit reporting start and end dates in YYYY-MM-DD format.",
74
+ "timeRange": "Explicit provider reporting period with since and until dates.",
75
+ "startDate": "First date of the reporting period, in YYYY-MM-DD format.",
76
+ "endDate": "Last date of the reporting period, in YYYY-MM-DD format, on or after startDate.",
77
+ "since": "Beginning of the requested reporting period; use the format accepted by the provider.",
78
+ "until": "End of the requested reporting period; use the format accepted by the provider.",
79
+ "orderBy": "Field used to order report rows.",
80
+ "orderField": "Provider field used to order report rows.",
81
+ "orderDirection": "Sort direction for the selected ordering field.",
82
+ "orderType": "Sort direction for the selected ordering field.",
83
+ "sortBy": "Metric or dimension used to rank the returned results.",
84
+ "sort": "Native provider ordering expressions.",
85
+ "limit": "Maximum number of returned items or rows. The declared bounds and default apply; use pagination for additional results.",
86
+ "topN": "Maximum number of highest-ranked results to return.",
87
+ "pageSize": "Maximum items requested per page. Follow the returned pagination information for remaining results.",
88
+ "page": "Page number to request, starting at 1.",
89
+ "offset": "Zero-based number of rows to skip before returning results.",
90
+ "bookmark": "Opaque Pinterest pagination bookmark returned by the previous response. Omit on the first request.",
91
+ "after": "Opaque cursor for the next page returned by Meta.",
92
+ "before": "Opaque cursor for the previous page returned by Meta.",
93
+ "includeUnchanged": "Include entries with no observed change in the comparison.",
94
+ "minImpressions": "Minimum reported impressions required for a row to enter the analysis.",
95
+ "objectives": "Analysis goals that determine the recommended large-site sampling strategy.",
96
+ "pageLimit": "Maximum number of pages considered for the sampling plan.",
97
+ "maxInspectionUrls": "Maximum URL Inspection candidates included in the plan; inspection calls consume provider quota.",
98
+ "minImpressionsForLowCtr": "Minimum impressions before a page is considered for low-CTR analysis.",
99
+ "staleSitemapDays": "Number of days after which a sitemap's last download is considered stale.",
100
+ "continueOnError": "Continue with other URLs when one inspection fails, returning individual failures.",
101
+ "dimension": "Reporting dimension used to group and compare results.",
102
+ "maxClusters": "Maximum query clusters to return.",
103
+ "topQueriesPerCluster": "Maximum example queries returned for each cluster.",
104
+ "minPages": "Minimum distinct pages ranking for a query before it is considered a cannibalization candidate.",
105
+ "adAccountId": "Owning advertising account ID. Use the exact ID returned by account discovery; do not substitute a campaign or business ID.",
106
+ "advertiserId": "TikTok advertiser account ID as a string.",
107
+ "customerId": "Google Ads customer ID, without hyphens.",
108
+ "propertyId": "Google Analytics property ID; use the property, not the account or measurement ID.",
109
+ "level": "Entity or aggregation level for the requested report.",
110
+ "dataLevel": "TikTok reporting aggregation level. Metrics and dimensions must be compatible with this level.",
111
+ "granularity": "Time bucket used to aggregate reporting rows.",
112
+ "columns": "Native Pinterest reporting column names. Discover supported columns before selecting metrics.",
113
+ "executionMode": "Choose the supported synchronous, asynchronous or automatic report execution strategy.",
114
+ "entityIds": "Exact IDs of the entities to include in the selected reporting level.",
115
+ "targetingTypes": "Native targeting categories used to break down delivery.",
116
+ "clickWindowDays": "Click-through attribution window, in days.",
117
+ "engagementWindowDays": "Engagement-through attribution window, in days.",
118
+ "viewWindowDays": "View-through attribution window, in days.",
119
+ "conversionReportTime": "Whether conversions are assigned to the ad action date or the conversion date.",
120
+ "attributionTypes": "Provider attribution categories included in the report.",
121
+ "reportingTimezone": "Provider reporting timezone selector. Keep it consistent when comparing periods.",
122
+ "campaignIds": "Restrict results to these campaign IDs.",
123
+ "adGroupIds": "Restrict results to these ad group IDs.",
124
+ "adgroupIds": "Restrict TikTok results to these ad group IDs.",
125
+ "adIds": "Restrict results to these ad IDs.",
126
+ "campaignId": "Exact campaign ID in the selected advertising account.",
127
+ "adGroupId": "Exact ad group ID in the selected advertising account.",
128
+ "adSetId": "Exact Meta ad set ID in the selected advertising account.",
129
+ "adId": "Exact ad ID in the selected advertising account.",
130
+ "entityId": "Exact ID of the entity selected by this operation. It must belong to the selected account.",
131
+ "onlyWithPeriodDelivery": "Keep only creatives with observed delivery in the requested date range.",
132
+ "breakdown": "Native reporting breakdown used to group results.",
133
+ "breakdowns": "Native reporting breakdowns. Check compatibility before combining them.",
134
+ "productGroupIds": "Restrict reporting to these product group IDs.",
135
+ "productItemIds": "Restrict reporting to these product item IDs.",
136
+ "includeProductSamples": "Include a limited sample of product records alongside inventory diagnostics.",
137
+ "sampleProductGroups": "Maximum product groups to sample for inventory diagnostics.",
138
+ "reportName": "Provider report name identifying the requested report recipe.",
139
+ "conversionProductAttributionType": "Pinterest product-conversion attribution category.",
140
+ "conversionProductBreakdown": "Pinterest product-conversion grouping level.",
141
+ "productSkuIds": "Restrict product-conversion reporting to these SKU IDs.",
142
+ "request": "Exact request schema name to inspect before composing a write payload.",
143
+ "entity": "Entity collection or level to inspect.",
144
+ "entityType": "Provider entity type to retrieve.",
145
+ "entityStatuses": "Native status values used to filter the entity collection.",
146
+ "statusFilter": "Native status values used to filter the returned entities.",
147
+ "targetingType": "Targeting dictionary to inspect, such as interests, locations or demographics.",
148
+ "interestId": "Native interest ID for the targeting lookup.",
149
+ "mode": "Operation or analysis mode. Only the values declared in this schema are supported.",
150
+ "matchTypes": "Keyword match types included in the lookup.",
151
+ "countryCode": "Country code used to localize keyword or targeting results.",
152
+ "keywords": "Keyword phrases to analyze; use the language and market of the intended audience.",
153
+ "term": "Search term used for the keyword lookup.",
154
+ "terms": "Search terms used for the keyword lookup.",
155
+ "audienceId": "Exact ID of the audience to inspect or modify.",
156
+ "customerListId": "Exact customer-list ID. Reading metadata does not upload customer data.",
157
+ "businessId": "Exact business account ID used for the lookup.",
158
+ "accountType": "Provider account category used to filter results.",
159
+ "ownershipType": "Filter audiences by provider ownership category.",
160
+ "excludeNca": "Whether to exclude Pinterest new-customer-acquisition audiences from this lookup.",
161
+ "audienceInsightType": "Pinterest audience-insights category to request.",
162
+ "surfaces": "Conversion-setup surfaces to inspect, according to the supported values.",
163
+ "includeDeletedTags": "Include deleted conversion tags in diagnostics.",
164
+ "lookbackPeriod": "Provider lookback period used for conversion diagnostics.",
165
+ "sourcePlatform": "Filter conversion data by source platform.",
166
+ "ingestionSource": "Filter conversion events by ingestion source.",
167
+ "catalogId": "Exact product catalog ID accessible to the selected account.",
168
+ "feedId": "Exact catalog feed ID.",
169
+ "processingResultId": "Exact catalog feed processing result ID to inspect.",
170
+ "productGroupId": "Exact product group ID.",
171
+ "productSetId": "Exact Meta product set ID belonging to the selected catalog.",
172
+ "exportType": "Provider export category to request.",
173
+ "action": "Supported operation within this tool's scoped provider surface.",
174
+ "includeDetails": "Include detailed provider records in addition to summary information.",
175
+ "leadFormId": "Exact lead form ID to inspect.",
176
+ "subscriptionId": "Exact subscription ID to inspect.",
177
+ "assetId": "Exact provider asset ID.",
178
+ "memberId": "Exact business member ID used for the read lookup.",
179
+ "partnerId": "Exact business partner ID used for the read lookup.",
180
+ "pinId": "Exact Pinterest pin ID.",
181
+ "pinIds": "Exact Pinterest pin IDs to include.",
182
+ "metrics": "Native or documented calculated metric names to request. Check compatibility with the selected dimensions.",
183
+ "metric": "Metric used for the analysis; use a name from the platform metric catalogue.",
184
+ "boardId": "Exact Pinterest board ID.",
185
+ "searchTerm": "Text used to filter or search the selected collection.",
186
+ "region": "Provider region selector used to localize trends.",
187
+ "productRegion": "Region used for product trend analysis.",
188
+ "trendType": "Provider trend category to return.",
189
+ "interests": "Native interest categories used to filter trends.",
190
+ "genders": "Native gender categories used to filter aggregate trends.",
191
+ "ageBuckets": "Native age groups used to filter aggregate trends.",
192
+ "includeKeywords": "Include related keyword details in the trend response.",
193
+ "includeDemographics": "Include available aggregate demographic breakdowns.",
194
+ "productCategories": "Native product category IDs or values used to filter trends.",
195
+ "verticals": "Native business verticals used to filter trends.",
196
+ "productLookbackDays": "Product-trend lookback window, in days.",
197
+ "productEngagementType": "Engagement signal used for product-trend analysis.",
198
+ "featuredInterest": "Featured interest category for the trends lookup.",
199
+ "reportType": "Native provider report type; determines supported metrics and dimensions.",
200
+ "promotionId": "Exact Pinterest product-group promotion ID.",
201
+ "type": "Provider object or operation type selected from this schema's allowed values.",
202
+ "productBreakdown": "Native product grouping used to join catalog items with reported insights.",
203
+ "catalogProductLimit": "Maximum catalog product records read for the join.",
204
+ "includeAdsets": "Include ad set configuration when checking brand safety.",
205
+ "includeBlockLists": "Include accessible brand-safety block-list metadata.",
206
+ "includeRawTargeting": "Include the native targeting specification for diagnostics.",
207
+ "cellEntityType": "Entity type represented by experiment cells.",
208
+ "includePagePosts": "Read related Facebook Page posts when the token has permission.",
209
+ "includeInstagramMedia": "Read related Instagram media when the token has permission.",
210
+ "includeInsights": "Include available insight metrics; additional provider permissions may be required.",
211
+ "edge": "Allowlisted Meta Graph edge to read on the selected node.",
212
+ "fields": "Native provider fields to return. Select only fields supported by the chosen object or report.",
213
+ "filtering": "Native provider filter expressions; credentials and account overrides are not allowed.",
214
+ "includeSummary": "Request the provider's available summary or total-count metadata.",
215
+ "actionBreakdowns": "Meta action-level breakdowns, subject to Insights compatibility rules.",
216
+ "actionAttributionWindows": "Meta attribution windows used to attribute action metrics.",
217
+ "timeIncrement": "Provider time-bucket size or supported aggregate value.",
218
+ "locationTypes": "Location dictionary categories to include in targeting search.",
219
+ "adFormat": "Native Meta ad preview format; must be compatible with this ad's creative.",
220
+ "handle": "Asynchronous catalog batch handle returned by the submission to inspect.",
221
+ "keepEmptyRows": "Keep provider rows whose selected metrics are all zero, where supported.",
222
+ "sampleLimit": "Maximum records included in the diagnostic sample.",
223
+ "isOpenFunnel": "Allow users to enter at any funnel step instead of requiring the first step.",
224
+ "visualizationType": "Supported funnel visualization mode.",
225
+ "breakdownLimit": "Maximum breakdown values to include in the funnel response.",
226
+ "nextActionLimit": "Maximum next actions returned for each funnel step.",
227
+ "fallbackToStepCounts": "If the advanced funnel is unavailable, return independent step counts with the limitation made explicit.",
228
+ "report": "Native GA4 report request object for this endpoint.",
229
+ "reports": "Native GA4 report request objects to execute in one batch.",
230
+ "dimensions": "Native reporting dimensions. Validate compatibility with the selected metrics.",
231
+ "compatibilityFilter": "Limit compatibility results to the requested GA4 compatibility category.",
232
+ "collection": "Allowlisted administrative resource collection to read.",
233
+ "settings": "Property configuration surfaces to inspect.",
234
+ "includeRecurring": "Include recurring audience-export configurations.",
235
+ "audienceExportName": "Full GA4 audience-export resource name returned by list_audience_exports.",
236
+ "queryLifetime": "Request lifetime reporting instead of the explicit reporting period where TikTok supports it.",
237
+ "declineThresholdPct": "Percentage decline used as a candidate signal for creative fatigue, requiring further evidence.",
238
+ "includeSavedAudiences": "Include accessible saved-audience metadata in the overlap analysis.",
239
+ "overlapThreshold": "Overlap threshold used to flag audience pairs, in the units declared by this schema.",
240
+ "serviceType": "TikTok report service category.",
241
+ "catalog": "Allowlisted targeting dictionary or catalog to inspect.",
242
+ "query": "Search text used to find matching entries in the selected catalogue.",
243
+ "parameters": "Native query parameters for this allowlisted read endpoint. Do not provide credentials or account overrides.",
244
+ "account_id": "Exact account ID selected for this platform in the GetMCPAds workspace.",
245
+ "platform": "Platform key identifying the connected advertising or analytics source.",
246
+ "id": "Exact saved object ID returned by the corresponding create or list tool.",
247
+ "revision": "Last known object revision, used to reject conflicting updates. Read the object before editing.",
248
+ "name": "Human-readable name for this object.",
249
+ "title": "Human-readable title for the resulting object or review.",
250
+ "clientLabel": "Display label identifying the client in the saved view.",
251
+ "config": "Complete saved-view configuration, including source, selected account, period and display options.",
252
+ "subject": "Entity or creative subject whose dated performance will be analyzed.",
253
+ "compare_previous": "Include the immediately preceding period of equal duration for comparison.",
254
+ "profile": "Creative brief profile containing the client's business context and analysis preferences.",
255
+ "primaryMetric": "Metric used to rank or evaluate the review; it must be available for the selected platform.",
256
+ "direction": "Whether higher or lower values of the primary metric represent improvement.",
257
+ "creative_ids": "Exact creative IDs to include in this review.",
258
+ "request_id": "Caller-generated request identifier. Reuse it for a retry of the same operation to avoid duplicate work.",
259
+ "expiresInDays": "Number of days before the public review sharing link expires.",
260
+ "client_id": "Exact saved business-client workspace ID.",
261
+ "review_id": "Exact review ID returned by the review creation or listing tool.",
262
+ "page_id": "Exact page identifier within the landing-page review.",
263
+ "entity_id": "Exact entity identifier in the selected review or platform context.",
264
+ "source_key": "Source identifier returned by the business review; keep the platform/account association intact.",
265
+ "metric_key": "Exact metric key in the review evidence.",
266
+ "follow_up_on": "Follow-up date for the decision, using the format declared in this schema.",
267
+ "hypothesis": "Specific hypothesis to record, supported by the review evidence.",
268
+ "successCriterion": "Measurable condition used to decide whether the recorded action succeeded.",
269
+ "outcome": "Observed outcome to record; distinguish evidence from an untested expectation.",
270
+ "status": "Status selected from the allowed values for this object or operation.",
271
+ "retry_source": "Source identifier to retry after reviewing its reported error.",
272
+ "site_origin": "Website origin to analyze, including https:// and hostname, without credentials or a path.",
273
+ "matchType": "Google Ads keyword match type for the forecast.",
274
+ "negativeKeywords": "Keyword phrases excluded from the keyword forecast.",
275
+ "negativeMatchType": "Match type applied to the negative keywords.",
276
+ "geoTargetIds": "Google Ads geo-target constant IDs for the intended locations.",
277
+ "languageId": "Google Ads language constant ID for the keyword request.",
278
+ "languageIds": "Google Ads language constant IDs for the intended audience.",
279
+ "network": "Google Ads search network selector used for planning.",
280
+ "biddingStrategy": "Supported bidding strategy for this temporary forecast. The request does not create a live campaign.",
281
+ "includeAdultKeywords": "Allow adult keyword ideas where supported by the provider and the selected market.",
282
+ "seedKeywords": "Seed keyword phrases from which Google generates keyword ideas.",
283
+ "includeAverageCpc": "Request average cost-per-click metrics when available.",
284
+ "includeDeviceBreakdown": "Include device-specific planning metrics when supported.",
285
+ "includeKeywordConcepts": "Include Google's keyword concept grouping metadata.",
286
+ "startYearMonth": "First year and month of the historical metrics period.",
287
+ "endYearMonth": "Last year and month of the historical metrics period.",
288
+ "includeDismissed": "Include recommendations already dismissed in the account.",
289
+ "operation": "Exact allowlisted operation to execute. Arbitrary provider operations are not accepted.",
290
+ "category": "Category selected from the provider dictionary or field catalogue.",
291
+ "selectable": "Filter the field catalogue by whether the field can appear in SELECT.",
292
+ "filterable": "Filter the field catalogue by whether the field can appear in WHERE.",
293
+ "sortable": "Filter the field catalogue by whether the field can appear in ORDER BY.",
294
+ "pageToken": "Opaque continuation token returned by the preceding provider response.",
295
+ "locationNames": "Human-readable location names for Google to resolve into geo-target constants.",
296
+ "locale": "Locale used for human-readable lookup results.",
297
+ "resource": "Native Google Ads resource from which the GAQL query selects rows.",
298
+ "cursor": "Opaque pagination cursor returned by the preceding response; omit for the first page.",
299
+ "kind": "Entity or request kind whose schema or data is being requested.",
300
+ "startTime": "Beginning of the reporting period as an ISO 8601 timestamp with an explicit timezone.",
301
+ "endTime": "End of the reporting period as an ISO 8601 timestamp with an explicit timezone, after startTime.",
302
+ "reportDimension": "Native dimension used to break down the Snapchat report."
303
+ };
304
+
305
+ // src/tool-quality.ts
306
+ import { z as z2 } from "zod";
307
+ var RESULT_SCHEMA = {
308
+ type: "object",
309
+ properties: {
310
+ result: {
311
+ 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.",
312
+ type: ["object", "array", "string", "number", "boolean", "null"]
313
+ }
314
+ },
315
+ required: ["result"],
316
+ additionalProperties: true
317
+ };
318
+ function toolAnnotations(write) {
319
+ return { readOnlyHint: !write, destructiveHint: write, idempotentHint: !write, openWorldHint: true };
320
+ }
321
+ function structuredResult(value) {
322
+ if (value.isError) return value;
323
+ let result = value.content;
324
+ const first = value.content[0];
325
+ if (value.content.length === 1 && first?.type === "text" && typeof first.text === "string") {
326
+ result = first.text;
327
+ try {
328
+ result = JSON.parse(first.text);
329
+ } catch {
330
+ }
331
+ }
332
+ return { ...value, structuredContent: { ...value.structuredContent, result } };
333
+ }
334
+ var resultShape = { result: z2.union([z2.object({}).passthrough(), z2.array(z2.unknown()), z2.string(), z2.number(), z2.boolean(), z2.null()]).describe(RESULT_SCHEMA.properties.result.description) };
335
+ function installToolQuality(server) {
336
+ server.tool = ((name, description, inputSchema, handler) => {
337
+ const write = Object.hasOwn(inputSchema, "confirm");
338
+ inputSchema = Object.fromEntries(Object.entries(inputSchema).map(([key, schema]) => [key, schema.description || PARAMETER_DESCRIPTIONS[key] ? schema.describe(schema.description || PARAMETER_DESCRIPTIONS[key]) : schema]));
339
+ return server.registerTool(name, {
340
+ title: name.replace(/_/g, " "),
341
+ description,
342
+ inputSchema,
343
+ outputSchema: resultShape,
344
+ annotations: toolAnnotations(write)
345
+ }, async (...args) => structuredResult(await handler(...args)));
346
+ });
347
+ }
348
+
67
349
  // src/server.ts
68
350
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
69
351
 
70
352
  // src/platforms/gsc/tools.ts
71
- import { z as z2 } from "zod";
353
+ import { z as z3 } from "zod";
72
354
 
73
355
  // src/core/errors.ts
74
356
  var PlatformApiError = class extends Error {
@@ -83,13 +365,6 @@ var PlatformApiError = class extends Error {
83
365
  this.retryAfter = retryAfter;
84
366
  this.name = "PlatformApiError";
85
367
  }
86
- platform;
87
- code;
88
- isRateLimit;
89
- isAuth;
90
- isPermission;
91
- suggestion;
92
- retryAfter;
93
368
  toMcpError() {
94
369
  return {
95
370
  error: this.message,
@@ -252,6 +527,33 @@ function flattenSearchAnalyticsResponse(response, dimensions) {
252
527
  }
253
528
 
254
529
  // src/platforms/gsc/gscClient.ts
530
+ function gscRouteClass(rawUrl) {
531
+ try {
532
+ const parsed = new URL(rawUrl);
533
+ if (parsed.pathname.endsWith("/searchAnalytics/query")) return "search_analytics";
534
+ if (parsed.pathname.endsWith("/urlInspection/index:inspect")) return "url_inspection";
535
+ if (parsed.pathname.includes("/sitemaps")) return "sitemaps";
536
+ if (parsed.pathname.includes("/sites")) return "sites";
537
+ } catch {
538
+ }
539
+ return "gsc_api";
540
+ }
541
+ function codeOwnedApiFailure(status) {
542
+ if (status === 401 || status === 403) {
543
+ return { message: "GSC API authorization failed.", status: "AUTHORIZATION_FAILED" };
544
+ }
545
+ if (status === 429) {
546
+ return { message: "GSC API rate limit was reached.", status: "RATE_LIMITED" };
547
+ }
548
+ if (status >= 500) {
549
+ return { message: "GSC API is temporarily unavailable.", status: "UPSTREAM_UNAVAILABLE" };
550
+ }
551
+ return { message: "GSC API rejected the request.", status: "REQUEST_REJECTED" };
552
+ }
553
+ var GSC_AUTO_PAGINATION_MAX_PAGES = 4;
554
+ var GSC_AUTO_PAGINATION_MAX_ROWS = 1e5;
555
+ var GSC_AUTO_PAGINATION_DEADLINE_MS = 3e4;
556
+ var GSC_SINGLE_REQUEST_TIMEOUT_MS = 9e4;
255
557
  function getAuthorizationUrl(clientId, redirectUri, state) {
256
558
  const params = new URLSearchParams({
257
559
  client_id: clientId,
@@ -325,16 +627,16 @@ var GSCClient = class {
325
627
  // ============================================
326
628
  // PRIVATE METHODS
327
629
  // ============================================
328
- async request(url, options = {}) {
630
+ async request(url, options = {}, timeoutMs = GSC_SINGLE_REQUEST_TIMEOUT_MS) {
631
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 1) {
632
+ throw new GSCApiException("GSC request deadline expired before dispatch", 408, "TIMEOUT");
633
+ }
329
634
  const controller = new AbortController();
330
- const timeout = setTimeout(() => controller.abort(), 9e4);
635
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
331
636
  try {
332
637
  const response = await fetch(url, {
333
638
  ...options,
334
639
  signal: controller.signal,
335
- // Forced after the spread: once a bearer token is attached, a redirect
336
- // must never be followed, or the credential would be forwarded to
337
- // whatever host the redirect names.
338
640
  redirect: "error",
339
641
  headers: {
340
642
  Authorization: `Bearer ${this.accessToken}`,
@@ -343,21 +645,22 @@ var GSCClient = class {
343
645
  }
344
646
  });
345
647
  if (!response.ok) {
346
- const errorBody = await response.json().catch(() => ({}));
347
- const errorDetails = errorBody?.error;
348
- console.error("[GSCClient] API Error:", response.status, JSON.stringify(errorBody, null, 2));
349
- const detailedMessage = errorDetails?.message || `Request failed: ${response.statusText}`;
648
+ const failure = codeOwnedApiFailure(response.status);
649
+ console.error("[GSCClient] API error", {
650
+ status: response.status,
651
+ route: gscRouteClass(url)
652
+ });
350
653
  throw new GSCApiException(
351
- detailedMessage,
654
+ failure.message,
352
655
  response.status,
353
- errorDetails?.status
656
+ failure.status
354
657
  );
355
658
  }
356
- return response.json();
659
+ return await response.json();
357
660
  } catch (error) {
358
661
  if (error instanceof DOMException && error.name === "AbortError") {
359
662
  throw new GSCApiException(
360
- "GSC API request timed out after 90 seconds",
663
+ "GSC API request exceeded its bounded deadline",
361
664
  408,
362
665
  "TIMEOUT"
363
666
  );
@@ -407,39 +710,80 @@ var GSCClient = class {
407
710
  const url = `${GSC_API_BASE}/webmasters/v3/sites/${encodedSiteUrl}/searchAnalytics/query`;
408
711
  const MAX_ROWS_PER_REQUEST = 25e3;
409
712
  const rowLimit = request.rowLimit ?? MAX_ROWS_PER_REQUEST;
410
- const initialOffset = request.startRow ?? 0;
713
+ const autoPaginate = request.rowLimit === void 0;
714
+ const paginationDeadline = Date.now() + GSC_AUTO_PAGINATION_DEADLINE_MS;
715
+ const remainingPaginationMs = () => {
716
+ const remaining = paginationDeadline - Date.now();
717
+ if (remaining < 1) {
718
+ throw new GSCApiException(
719
+ "GSC auto-pagination exceeded its 30-second total deadline",
720
+ 408,
721
+ "PAGINATION_DEADLINE"
722
+ );
723
+ }
724
+ return remaining;
725
+ };
411
726
  const firstResponse = await this.request(url, {
412
727
  method: "POST",
413
728
  body: JSON.stringify({
414
729
  ...request,
415
730
  rowLimit: Math.min(rowLimit, MAX_ROWS_PER_REQUEST),
416
- startRow: initialOffset
731
+ startRow: request.startRow ?? 0
417
732
  })
418
- });
733
+ }, autoPaginate ? remainingPaginationMs() : GSC_SINGLE_REQUEST_TIMEOUT_MS);
734
+ if (autoPaginate) remainingPaginationMs();
419
735
  const returnedRows = firstResponse.rows?.length ?? 0;
420
736
  if (request.rowLimit || returnedRows < MAX_ROWS_PER_REQUEST) {
421
- return firstResponse;
737
+ return {
738
+ ...firstResponse,
739
+ pagination: {
740
+ mode: autoPaginate ? "bounded_auto" : "single_page",
741
+ pageCount: 1,
742
+ rowCount: returnedRows,
743
+ maximumPages: autoPaginate ? GSC_AUTO_PAGINATION_MAX_PAGES : 1,
744
+ maximumRows: autoPaginate ? GSC_AUTO_PAGINATION_MAX_ROWS : rowLimit,
745
+ deadlineMs: autoPaginate ? GSC_AUTO_PAGINATION_DEADLINE_MS : GSC_SINGLE_REQUEST_TIMEOUT_MS,
746
+ truncated: false
747
+ }
748
+ };
422
749
  }
423
750
  const allRows = [...firstResponse.rows ?? []];
424
- let currentOffset = initialOffset + returnedRows;
425
- while (true) {
751
+ let currentOffset = (request.startRow ?? 0) + returnedRows;
752
+ let pageCount = 1;
753
+ let lastPageWasFull = returnedRows === MAX_ROWS_PER_REQUEST;
754
+ while (pageCount < GSC_AUTO_PAGINATION_MAX_PAGES && allRows.length < GSC_AUTO_PAGINATION_MAX_ROWS) {
755
+ const remainingRows = GSC_AUTO_PAGINATION_MAX_ROWS - allRows.length;
756
+ const requestedRows = Math.min(MAX_ROWS_PER_REQUEST, remainingRows);
426
757
  const pageResponse = await this.request(url, {
427
758
  method: "POST",
428
759
  body: JSON.stringify({
429
760
  ...request,
430
- rowLimit: MAX_ROWS_PER_REQUEST,
761
+ rowLimit: requestedRows,
431
762
  startRow: currentOffset
432
763
  })
433
- });
764
+ }, remainingPaginationMs());
765
+ remainingPaginationMs();
434
766
  const pageRows = pageResponse.rows?.length ?? 0;
767
+ pageCount += 1;
768
+ lastPageWasFull = pageRows === requestedRows;
435
769
  if (pageRows === 0) break;
436
- allRows.push(...pageResponse.rows ?? []);
770
+ allRows.push(...(pageResponse.rows ?? []).slice(0, remainingRows));
437
771
  currentOffset += pageRows;
438
- if (pageRows < MAX_ROWS_PER_REQUEST) break;
772
+ if (pageRows < requestedRows) break;
439
773
  }
774
+ remainingPaginationMs();
440
775
  return {
441
776
  ...firstResponse,
442
- rows: allRows
777
+ rows: allRows.slice(0, GSC_AUTO_PAGINATION_MAX_ROWS),
778
+ pagination: {
779
+ mode: "bounded_auto",
780
+ pageCount,
781
+ rowCount: Math.min(allRows.length, GSC_AUTO_PAGINATION_MAX_ROWS),
782
+ maximumPages: GSC_AUTO_PAGINATION_MAX_PAGES,
783
+ maximumRows: GSC_AUTO_PAGINATION_MAX_ROWS,
784
+ deadlineMs: GSC_AUTO_PAGINATION_DEADLINE_MS,
785
+ truncated: lastPageWasFull && (pageCount >= GSC_AUTO_PAGINATION_MAX_PAGES || allRows.length >= GSC_AUTO_PAGINATION_MAX_ROWS)
786
+ }
443
787
  };
444
788
  }
445
789
  // ============================================
@@ -880,36 +1224,37 @@ function planGSCQuery(params) {
880
1224
  }
881
1225
 
882
1226
  // src/platforms/gsc/tools.ts
883
- 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.");
884
- var datePresetSchema = z2.enum(["today", "yesterday", "last7days", "last28days", "last3months", "last6months", "last12months", "last16months"]);
885
- var searchTypeSchema = z2.enum(["web", "image", "video", "news", "discover", "googleNews"]);
886
- var dataStateSchema = z2.enum(["final", "all", "hourly_all"]);
887
- var aggregationTypeSchema = z2.enum(["auto", "byPage", "byProperty", "byNewsShowcasePanel"]);
888
- var limitSchema = z2.number().int().min(1).max(25e3).optional().default(1e3);
889
- var dateRangeSchema = z2.object({
890
- startDate: z2.string().describe("Start date YYYY-MM-DD"),
891
- endDate: z2.string().describe("End date YYYY-MM-DD")
1227
+ var siteUrlSchema = z3.string().optional().describe("Search Console property URL, e.g. https://example.com/ or sc-domain:example.com. Uses GSC_SITE_URL when omitted.");
1228
+ var datePresetSchema = z3.enum(["today", "yesterday", "last7days", "last28days", "last3months", "last6months", "last12months", "last16months"]);
1229
+ var searchTypeSchema = z3.enum(["web", "image", "video", "news", "discover", "googleNews"]);
1230
+ var dataStateSchema = z3.enum(["final", "all", "hourly_all"]);
1231
+ var aggregationTypeSchema = z3.enum(["auto", "byPage", "byProperty", "byNewsShowcasePanel"]);
1232
+ var CompareSearchTypesSchema = z3.array(searchTypeSchema).min(1).max(6).refine((items) => new Set(items).size === items.length, "searchTypes must be unique");
1233
+ var limitSchema = z3.number().int().min(1).max(25e3).optional().default(1e3);
1234
+ var dateRangeSchema = z3.object({
1235
+ startDate: z3.string().describe("Start date YYYY-MM-DD"),
1236
+ endDate: z3.string().describe("End date YYYY-MM-DD")
892
1237
  });
893
- var comparisonDimensionSchema = z2.enum(["query", "page"]);
894
- var sitemapBaselineSchema = z2.object({
895
- path: z2.string().describe("Sitemap URL/path returned by GSC."),
896
- submitted: z2.number().int().min(0).optional().default(0),
897
- indexed: z2.number().int().min(0).optional().default(0),
898
- warnings: z2.number().int().min(0).optional().default(0),
899
- errors: z2.number().int().min(0).optional().default(0),
900
- lastSubmitted: z2.string().optional(),
901
- lastDownloaded: z2.string().optional()
1238
+ var comparisonDimensionSchema = z3.enum(["query", "page"]);
1239
+ var sitemapBaselineSchema = z3.object({
1240
+ path: z3.string().describe("Sitemap URL/path returned by GSC."),
1241
+ submitted: z3.number().int().min(0).optional().default(0),
1242
+ indexed: z3.number().int().min(0).optional().default(0),
1243
+ warnings: z3.number().int().min(0).optional().default(0),
1244
+ errors: z3.number().int().min(0).optional().default(0),
1245
+ lastSubmitted: z3.string().optional(),
1246
+ lastDownloaded: z3.string().optional()
902
1247
  });
903
- var watchlistUrlSchema = z2.object({
904
- url: z2.string().url(),
905
- label: z2.string().optional(),
906
- priority: z2.enum(["low", "medium", "high", "critical"]).optional().default("medium"),
907
- tags: z2.array(z2.string()).optional().default([]),
908
- expectedVerdict: z2.enum(["PASS", "PARTIAL", "FAIL", "NEUTRAL", "VERDICT_UNSPECIFIED"]).optional().default("PASS"),
909
- expectedCanonical: z2.string().url().optional(),
910
- expectedCoverageState: z2.string().optional()
1248
+ var watchlistUrlSchema = z3.object({
1249
+ url: z3.string().url(),
1250
+ label: z3.string().optional(),
1251
+ priority: z3.enum(["low", "medium", "high", "critical"]).optional().default("medium"),
1252
+ tags: z3.array(z3.string()).optional().default([]),
1253
+ expectedVerdict: z3.enum(["PASS", "PARTIAL", "FAIL", "NEUTRAL", "VERDICT_UNSPECIFIED"]).optional().default("PASS"),
1254
+ expectedCanonical: z3.string().url().optional(),
1255
+ expectedCoverageState: z3.string().optional()
911
1256
  });
912
- var samplingObjectiveSchema = z2.enum(["topTraffic", "lowCtr", "declining", "sitemapRisk", "staleSitemaps"]);
1257
+ var samplingObjectiveSchema = z3.enum(["topTraffic", "lowCtr", "declining", "sitemapRisk", "staleSitemaps"]);
913
1258
  var BULK_URL_INSPECTION_LIMIT = 10;
914
1259
  var URL_INSPECTION_DAILY_QUOTA = 2e3;
915
1260
  var DEFAULT_FRESHNESS_LAG_WARNING_DAYS = 3;
@@ -1040,7 +1385,6 @@ var AuthenticatedGSCClient = class {
1040
1385
  this.tokenExpiresAt = Date.now() + 45 * 60 * 1e3;
1041
1386
  }
1042
1387
  }
1043
- config;
1044
1388
  accessToken;
1045
1389
  tokenExpiresAt = 0;
1046
1390
  rateLimiter = new RateLimiter();
@@ -1570,26 +1914,26 @@ function registerGSCTools(server, config) {
1570
1914
  Use gsc://metrics, gsc://dimensions, gsc://filters, and gsc://compatibility for available fields and rules.`,
1571
1915
  {
1572
1916
  siteUrl: siteUrlSchema,
1573
- metrics: z2.array(z2.string()).optional().describe("Native: clicks, impressions, ctr, position. Calculated: ctrOpportunity, clicksPerImpression, impressionShare, clickShare."),
1574
- dimensions: z2.array(z2.string()).max(5).optional().describe("Up to 5 dimensions selected from query, page, country, device, date, hour, searchAppearance."),
1575
- filters: z2.array(z2.object({
1576
- field: z2.string(),
1577
- operator: z2.enum(["contains", "equals", "notContains", "notEquals", "includingRegex", "excludingRegex"]),
1578
- value: z2.union([z2.string(), z2.number(), z2.boolean()])
1917
+ metrics: z3.array(z3.string()).optional().describe("Native: clicks, impressions, ctr, position. Calculated: ctrOpportunity, clicksPerImpression, impressionShare, clickShare."),
1918
+ dimensions: z3.array(z3.string()).max(5).optional().describe("Up to 5 dimensions selected from query, page, country, device, date, hour, searchAppearance."),
1919
+ filters: z3.array(z3.object({
1920
+ field: z3.string(),
1921
+ operator: z3.enum(["contains", "equals", "notContains", "notEquals", "includingRegex", "excludingRegex"]),
1922
+ value: z3.union([z3.string(), z3.number(), z3.boolean()])
1579
1923
  })).optional(),
1580
1924
  searchType: searchTypeSchema.optional().default("web"),
1581
1925
  dataState: dataStateSchema.optional().default("all").describe("final=stable data, all=fresh daily data, hourly_all=fresh hourly data (use hour dimension)."),
1582
1926
  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."),
1583
1927
  datePreset: datePresetSchema.optional().default("last28days"),
1584
- dateRange: z2.object({
1585
- startDate: z2.string().describe("Start date YYYY-MM-DD"),
1586
- endDate: z2.string().describe("End date YYYY-MM-DD")
1928
+ dateRange: z3.object({
1929
+ startDate: z3.string().describe("Start date YYYY-MM-DD"),
1930
+ endDate: z3.string().describe("End date YYYY-MM-DD")
1587
1931
  }).optional(),
1588
- orderBy: z2.enum(["clicks", "impressions", "ctr", "position", "ctrOpportunity", "clicksPerImpression", "impressionShare", "clickShare"]).optional().default("clicks"),
1589
- orderDirection: z2.enum(["ASC", "DESC"]).optional().default("DESC"),
1932
+ orderBy: z3.enum(["clicks", "impressions", "ctr", "position", "ctrOpportunity", "clicksPerImpression", "impressionShare", "clickShare"]).optional().default("clicks"),
1933
+ orderDirection: z3.enum(["ASC", "DESC"]).optional().default("DESC"),
1590
1934
  limit: limitSchema,
1591
- startRow: z2.number().int().min(0).optional().describe("Optional 0-based row offset for explicit pagination."),
1592
- autoPaginate: z2.boolean().optional().default(false).describe("When true, omit rowLimit so the GSC client auto-paginates until exhaustion.")
1935
+ startRow: z3.number().int().min(0).optional().describe("Optional 0-based row offset for explicit pagination."),
1936
+ autoPaginate: z3.boolean().optional().default(false).describe("When true, omit rowLimit so the GSC client auto-paginates until exhaustion.")
1593
1937
  },
1594
1938
  async ({ siteUrl, metrics, dimensions, filters, searchType, dataState, aggregationType, datePreset, dateRange, orderBy, orderDirection, limit, startRow, autoPaginate }) => {
1595
1939
  try {
@@ -1639,6 +1983,11 @@ Use gsc://metrics, gsc://dimensions, gsc://filters, and gsc://compatibility for
1639
1983
  mode: autoPaginate ? "auto_startRow" : "single_page",
1640
1984
  autoPaginate,
1641
1985
  startRow: startRow ?? 0,
1986
+ pageCount: response.pagination?.pageCount ?? 1,
1987
+ maximumPages: response.pagination?.maximumPages ?? 1,
1988
+ maximumRows: response.pagination?.maximumRows ?? limit,
1989
+ deadlineMs: response.pagination?.deadlineMs ?? 9e4,
1990
+ truncated: response.pagination?.truncated ?? false,
1642
1991
  limit: autoPaginate ? null : limit
1643
1992
  },
1644
1993
  dataState,
@@ -1655,8 +2004,8 @@ Use gsc://metrics, gsc://dimensions, gsc://filters, and gsc://compatibility for
1655
2004
  "Inspect a URL for Google indexing status, crawl info, mobile usability, AMP, and rich results. Quota: 2000 requests/day/property.",
1656
2005
  {
1657
2006
  siteUrl: siteUrlSchema,
1658
- url: z2.string().url().describe("Full URL to inspect."),
1659
- languageCode: z2.string().optional().describe("Optional IETF language tag, e.g. en-US or fr-FR.")
2007
+ url: z3.string().url().describe("Full URL to inspect."),
2008
+ languageCode: z3.string().optional().describe("Optional IETF language tag, e.g. en-US or fr-FR.")
1660
2009
  },
1661
2010
  async ({ siteUrl, url, languageCode }) => {
1662
2011
  try {
@@ -1681,9 +2030,9 @@ Use gsc://metrics, gsc://dimensions, gsc://filters, and gsc://compatibility for
1681
2030
  "Inspect multiple URLs with a conservative sequential limit and normalized URL Inspection results. Quota: 2000 requests/day/property.",
1682
2031
  {
1683
2032
  siteUrl: siteUrlSchema,
1684
- 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.`),
1685
- languageCode: z2.string().optional().describe("Optional IETF language tag, e.g. en-US or fr-FR."),
1686
- continueOnError: z2.boolean().optional().default(true).describe("When true, returns per-URL errors instead of failing the entire batch.")
2033
+ urls: z3.array(z3.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.`),
2034
+ languageCode: z3.string().optional().describe("Optional IETF language tag, e.g. en-US or fr-FR."),
2035
+ continueOnError: z3.boolean().optional().default(true).describe("When true, returns per-URL errors instead of failing the entire batch.")
1687
2036
  },
1688
2037
  async ({ siteUrl, urls, languageCode, continueOnError }) => {
1689
2038
  try {
@@ -1723,7 +2072,7 @@ Use gsc://metrics, gsc://dimensions, gsc://filters, and gsc://compatibility for
1723
2072
  "List sitemaps submitted for a Search Console property, including processing status and submitted/indexed counts.",
1724
2073
  {
1725
2074
  siteUrl: siteUrlSchema,
1726
- sitemapIndex: z2.string().url().optional().describe("Optional sitemap index URL used to return only its child sitemaps.")
2075
+ sitemapIndex: z3.string().url().optional().describe("Optional sitemap index URL used to return only its child sitemaps.")
1727
2076
  },
1728
2077
  async ({ siteUrl, sitemapIndex }) => {
1729
2078
  try {
@@ -1751,7 +2100,7 @@ Use gsc://metrics, gsc://dimensions, gsc://filters, and gsc://compatibility for
1751
2100
  "Get one submitted sitemap by full feed path, including type, fetch state, warnings/errors, and submitted/indexed content totals.",
1752
2101
  {
1753
2102
  siteUrl: siteUrlSchema,
1754
- feedpath: z2.string().url().describe("Full sitemap URL exactly as submitted to Search Console.")
2103
+ feedpath: z3.string().url().describe("Full sitemap URL exactly as submitted to Search Console.")
1755
2104
  },
1756
2105
  async ({ siteUrl, feedpath }) => {
1757
2106
  try {
@@ -1828,9 +2177,9 @@ Use gsc://metrics, gsc://dimensions, gsc://filters, and gsc://compatibility for
1828
2177
  "Compare performance across Search Console search types: web, image, video, news, discover, and Google News.",
1829
2178
  {
1830
2179
  siteUrl: siteUrlSchema,
1831
- searchTypes: z2.array(searchTypeSchema).optional().default(["web", "image", "video", "discover"]),
2180
+ searchTypes: CompareSearchTypesSchema.optional().default(["web", "image", "video", "discover"]),
1832
2181
  datePreset: datePresetSchema.optional().default("last28days"),
1833
- dateRange: z2.object({ startDate: z2.string(), endDate: z2.string() }).optional()
2182
+ dateRange: z3.object({ startDate: z3.string(), endDate: z3.string() }).optional()
1834
2183
  },
1835
2184
  async ({ siteUrl, searchTypes, datePreset, dateRange }) => {
1836
2185
  try {
@@ -1881,7 +2230,7 @@ Use gsc://metrics, gsc://dimensions, gsc://filters, and gsc://compatibility for
1881
2230
  {
1882
2231
  siteUrl: siteUrlSchema,
1883
2232
  searchType: searchTypeSchema.optional().default("web"),
1884
- lookbackDays: z2.number().int().min(3).max(30).optional().default(14).describe("Recent days to scan for daily data. Max 30.")
2233
+ lookbackDays: z3.number().int().min(3).max(30).optional().default(14).describe("Recent days to scan for daily data. Max 30.")
1885
2234
  },
1886
2235
  async ({ siteUrl, searchType, lookbackDays }) => {
1887
2236
  try {
@@ -1939,11 +2288,11 @@ Use gsc://metrics, gsc://dimensions, gsc://filters, and gsc://compatibility for
1939
2288
  "Read-only monitoring snapshot for Search Analytics freshness, sitemap indexation totals, and an optional small URL Inspection sample.",
1940
2289
  {
1941
2290
  siteUrl: siteUrlSchema,
1942
- searchTypes: z2.array(searchTypeSchema).min(1).max(6).optional().default(["web"]),
1943
- lookbackDays: z2.number().int().min(3).max(30).optional().default(14).describe("Recent days to scan for Search Analytics freshness."),
1944
- 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."),
1945
- 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.`),
1946
- languageCode: z2.string().optional().describe("Optional IETF language tag for URL Inspection, e.g. en-US or fr-FR.")
2291
+ searchTypes: z3.array(searchTypeSchema).min(1).max(6).optional().default(["web"]),
2292
+ lookbackDays: z3.number().int().min(3).max(30).optional().default(14).describe("Recent days to scan for Search Analytics freshness."),
2293
+ freshnessLagWarningDays: z3.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."),
2294
+ inspectUrls: z3.array(z3.string().url()).max(BULK_URL_INSPECTION_LIMIT).optional().default([]).describe(`Optional URLs to inspect. Maximum ${BULK_URL_INSPECTION_LIMIT} to protect URL Inspection quota.`),
2295
+ languageCode: z3.string().optional().describe("Optional IETF language tag for URL Inspection, e.g. en-US or fr-FR.")
1947
2296
  },
1948
2297
  async ({ siteUrl, searchTypes, lookbackDays, freshnessLagWarningDays, inspectUrls, languageCode }) => {
1949
2298
  try {
@@ -2046,9 +2395,9 @@ Use gsc://metrics, gsc://dimensions, gsc://filters, and gsc://compatibility for
2046
2395
  "Read-only sitemap delta tracker. Fetches the current sitemap snapshot and compares it to an agent-provided baseline; no state is written.",
2047
2396
  {
2048
2397
  siteUrl: siteUrlSchema,
2049
- baseline: z2.array(sitemapBaselineSchema).optional().default([]).describe("Previous snapshot entries from this tool's snapshot output."),
2050
- includeUnchanged: z2.boolean().optional().default(false),
2051
- minAbsIndexedDelta: z2.number().int().min(0).optional().default(1).describe("Minimum absolute indexed URL delta to include otherwise unchanged sitemaps.")
2398
+ baseline: z3.array(sitemapBaselineSchema).optional().default([]).describe("Previous snapshot entries from this tool's snapshot output."),
2399
+ includeUnchanged: z3.boolean().optional().default(false),
2400
+ minAbsIndexedDelta: z3.number().int().min(0).optional().default(1).describe("Minimum absolute indexed URL delta to include otherwise unchanged sitemaps.")
2052
2401
  },
2053
2402
  async ({ siteUrl, baseline, includeUnchanged, minAbsIndexedDelta }) => {
2054
2403
  try {
@@ -2143,11 +2492,11 @@ Use gsc://metrics, gsc://dimensions, gsc://filters, and gsc://compatibility for
2143
2492
  datePreset: datePresetSchema.optional().default("last28days"),
2144
2493
  currentDateRange: dateRangeSchema.optional().describe("Current period. Defaults to datePreset."),
2145
2494
  previousDateRange: dateRangeSchema.optional().describe("Previous period. Defaults to the immediately preceding period with the same length."),
2146
- includePages: z2.boolean().optional().default(false).describe("When true, groups by searchAppearance + page for page-level rich result trends."),
2147
- limit: z2.number().int().min(1).max(25e3).optional().default(5e3),
2148
- topN: z2.number().int().min(1).max(100).optional().default(50),
2149
- minImpressions: z2.number().int().min(0).optional().default(0),
2150
- sortBy: z2.enum(["currentClicks", "currentImpressions", "clicksDelta", "impressionsDelta", "positionDelta"]).optional().default("impressionsDelta")
2495
+ includePages: z3.boolean().optional().default(false).describe("When true, groups by searchAppearance + page for page-level rich result trends."),
2496
+ limit: z3.number().int().min(1).max(25e3).optional().default(5e3),
2497
+ topN: z3.number().int().min(1).max(100).optional().default(50),
2498
+ minImpressions: z3.number().int().min(0).optional().default(0),
2499
+ sortBy: z3.enum(["currentClicks", "currentImpressions", "clicksDelta", "impressionsDelta", "positionDelta"]).optional().default("impressionsDelta")
2151
2500
  },
2152
2501
  async ({ siteUrl, searchType, datePreset, currentDateRange, previousDateRange, includePages, limit, topN, minImpressions, sortBy }) => {
2153
2502
  try {
@@ -2257,11 +2606,11 @@ Use gsc://metrics, gsc://dimensions, gsc://filters, and gsc://compatibility for
2257
2606
  searchType: searchTypeSchema.optional().default("web"),
2258
2607
  datePreset: datePresetSchema.optional().default("last28days"),
2259
2608
  dateRange: dateRangeSchema.optional(),
2260
- objectives: z2.array(samplingObjectiveSchema).min(1).max(5).optional().default(["topTraffic", "lowCtr", "declining", "sitemapRisk", "staleSitemaps"]),
2261
- pageLimit: z2.number().int().min(1).max(25e3).optional().default(5e3),
2262
- maxInspectionUrls: z2.number().int().min(1).max(BULK_URL_INSPECTION_LIMIT).optional().default(BULK_URL_INSPECTION_LIMIT),
2263
- minImpressionsForLowCtr: z2.number().int().min(1).optional().default(100),
2264
- staleSitemapDays: z2.number().int().min(1).max(180).optional().default(14)
2609
+ objectives: z3.array(samplingObjectiveSchema).min(1).max(5).optional().default(["topTraffic", "lowCtr", "declining", "sitemapRisk", "staleSitemaps"]),
2610
+ pageLimit: z3.number().int().min(1).max(25e3).optional().default(5e3),
2611
+ maxInspectionUrls: z3.number().int().min(1).max(BULK_URL_INSPECTION_LIMIT).optional().default(BULK_URL_INSPECTION_LIMIT),
2612
+ minImpressionsForLowCtr: z3.number().int().min(1).optional().default(100),
2613
+ staleSitemapDays: z3.number().int().min(1).max(180).optional().default(14)
2265
2614
  },
2266
2615
  async ({ siteUrl, searchType, datePreset, dateRange, objectives, pageLimit, maxInspectionUrls, minImpressionsForLowCtr, staleSitemapDays }) => {
2267
2616
  try {
@@ -2390,9 +2739,9 @@ Use gsc://metrics, gsc://dimensions, gsc://filters, and gsc://compatibility for
2390
2739
  "Read-only URL Inspection watchlist for critical URLs. Inspects up to 10 URLs and returns alert/review/ok assessments without storing state.",
2391
2740
  {
2392
2741
  siteUrl: siteUrlSchema,
2393
- 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.`),
2394
- languageCode: z2.string().optional().describe("Optional IETF language tag, e.g. en-US or fr-FR."),
2395
- continueOnError: z2.boolean().optional().default(true)
2742
+ urls: z3.array(watchlistUrlSchema).min(1).max(BULK_URL_INSPECTION_LIMIT).describe(`Watchlist entries. Maximum ${BULK_URL_INSPECTION_LIMIT} per call to protect URL Inspection quota.`),
2743
+ languageCode: z3.string().optional().describe("Optional IETF language tag, e.g. en-US or fr-FR."),
2744
+ continueOnError: z3.boolean().optional().default(true)
2396
2745
  },
2397
2746
  async ({ siteUrl, urls, languageCode, continueOnError }) => {
2398
2747
  try {
@@ -2459,10 +2808,10 @@ Use gsc://metrics, gsc://dimensions, gsc://filters, and gsc://compatibility for
2459
2808
  datePreset: datePresetSchema.optional().default("last28days"),
2460
2809
  currentDateRange: dateRangeSchema.optional().describe("Current period. Defaults to datePreset."),
2461
2810
  previousDateRange: dateRangeSchema.optional().describe("Previous period. Defaults to the immediately preceding period with the same length."),
2462
- limit: z2.number().int().min(1).max(25e3).optional().default(5e3).describe("Rows to fetch for each period."),
2463
- topN: z2.number().int().min(1).max(100).optional().default(25),
2464
- minAbsClickDelta: z2.number().min(0).optional().default(0).describe("Filter out rows with smaller absolute click delta."),
2465
- sortBy: z2.enum(["clicksDelta", "impressionsDelta", "ctrDelta", "positionDelta"]).optional().default("clicksDelta")
2811
+ limit: z3.number().int().min(1).max(25e3).optional().default(5e3).describe("Rows to fetch for each period."),
2812
+ topN: z3.number().int().min(1).max(100).optional().default(25),
2813
+ minAbsClickDelta: z3.number().min(0).optional().default(0).describe("Filter out rows with smaller absolute click delta."),
2814
+ sortBy: z3.enum(["clicksDelta", "impressionsDelta", "ctrDelta", "positionDelta"]).optional().default("clicksDelta")
2466
2815
  },
2467
2816
  async ({ siteUrl, dimension, searchType, datePreset, currentDateRange, previousDateRange, limit, topN, minAbsClickDelta, sortBy }) => {
2468
2817
  try {
@@ -2578,11 +2927,11 @@ Use gsc://metrics, gsc://dimensions, gsc://filters, and gsc://compatibility for
2578
2927
  searchType: searchTypeSchema.optional().default("web"),
2579
2928
  datePreset: datePresetSchema.optional().default("last28days"),
2580
2929
  dateRange: dateRangeSchema.optional(),
2581
- brandTerms: z2.array(z2.string()).optional().default([]).describe("Optional brand terms. Defaults to terms derived from the property domain."),
2582
- categoryRules: z2.record(z2.array(z2.string())).optional().describe("Optional map of category name to matching tokens/phrases."),
2583
- limit: z2.number().int().min(1).max(25e3).optional().default(5e3),
2584
- maxClusters: z2.number().int().min(1).max(100).optional().default(25),
2585
- topQueriesPerCluster: z2.number().int().min(1).max(25).optional().default(10)
2930
+ brandTerms: z3.array(z3.string()).optional().default([]).describe("Optional brand terms. Defaults to terms derived from the property domain."),
2931
+ categoryRules: z3.record(z3.array(z3.string())).optional().describe("Optional map of category name to matching tokens/phrases."),
2932
+ limit: z3.number().int().min(1).max(25e3).optional().default(5e3),
2933
+ maxClusters: z3.number().int().min(1).max(100).optional().default(25),
2934
+ topQueriesPerCluster: z3.number().int().min(1).max(25).optional().default(10)
2586
2935
  },
2587
2936
  async ({ siteUrl, searchType, datePreset, dateRange, brandTerms, categoryRules, limit, maxClusters, topQueriesPerCluster }) => {
2588
2937
  try {
@@ -2659,11 +3008,11 @@ Use gsc://metrics, gsc://dimensions, gsc://filters, and gsc://compatibility for
2659
3008
  searchType: searchTypeSchema.optional().default("web"),
2660
3009
  datePreset: datePresetSchema.optional().default("last28days"),
2661
3010
  dateRange: dateRangeSchema.optional(),
2662
- limit: z2.number().int().min(1).max(25e3).optional().default(5e3),
2663
- minPages: z2.number().int().min(2).max(10).optional().default(2),
2664
- minImpressions: z2.number().int().min(0).optional().default(100),
2665
- 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."),
2666
- topN: z2.number().int().min(1).max(100).optional().default(25)
3011
+ limit: z3.number().int().min(1).max(25e3).optional().default(5e3),
3012
+ minPages: z3.number().int().min(2).max(10).optional().default(2),
3013
+ minImpressions: z3.number().int().min(0).optional().default(100),
3014
+ maxTopPageClickShare: z3.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."),
3015
+ topN: z3.number().int().min(1).max(100).optional().default(25)
2667
3016
  },
2668
3017
  async ({ siteUrl, searchType, datePreset, dateRange, limit, minPages, minImpressions, maxTopPageClickShare, topN }) => {
2669
3018
  try {
@@ -2759,9 +3108,9 @@ Use gsc://metrics, gsc://dimensions, gsc://filters, and gsc://compatibility for
2759
3108
  "gsc_validate_query",
2760
3109
  "Validate a Search Console metric/dimension/search type combination before executing it.",
2761
3110
  {
2762
- dimensions: z2.array(z2.string()).optional().describe("Dimensions to validate."),
3111
+ dimensions: z3.array(z3.string()).optional().describe("Dimensions to validate."),
2763
3112
  searchType: searchTypeSchema.optional().default("web"),
2764
- dateRange: z2.object({ startDate: z2.string(), endDate: z2.string() }).optional()
3113
+ dateRange: z3.object({ startDate: z3.string(), endDate: z3.string() }).optional()
2765
3114
  },
2766
3115
  async ({ dimensions, searchType, dateRange }) => {
2767
3116
  try {
@@ -3044,12 +3393,13 @@ function registerGSC(server, config) {
3044
3393
  }
3045
3394
 
3046
3395
  // src/server.ts
3047
- var PACKAGE_VERSION = "1.0.2";
3396
+ var PACKAGE_VERSION = "2.0.1";
3048
3397
  function createServer(config) {
3049
3398
  const server = new McpServer(
3050
- { name: "google-search-console-mcp", version: PACKAGE_VERSION },
3399
+ { 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" }] },
3051
3400
  { capabilities: { tools: { listChanged: true }, resources: { subscribe: false, listChanged: true } } }
3052
3401
  );
3402
+ installToolQuality(server);
3053
3403
  registerGSC(server, config);
3054
3404
  logger.system(`google-search-console-mcp v${PACKAGE_VERSION} ready, read-only`);
3055
3405
  return server;