@digestseo/mcp-gsc 0.4.0 → 0.4.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/CHANGELOG.md +14 -0
- package/README.md +6 -4
- package/package.json +1 -1
- package/src/digest.ts +60 -24
- package/src/google.ts +156 -27
- package/src/index.ts +303 -72
- package/src/quick-wins-schema.ts +17 -0
- package/src/result-bounds.ts +91 -0
package/src/index.ts
CHANGED
|
@@ -66,6 +66,13 @@ import {
|
|
|
66
66
|
assertIndexingRequestUrl,
|
|
67
67
|
assertIndexingUrlAuthorized,
|
|
68
68
|
} from './indexing-property-authorization';
|
|
69
|
+
import {
|
|
70
|
+
DEFAULT_ANALYSIS_RESULT_LIMIT,
|
|
71
|
+
MAX_ANALYSIS_RESULT_LIMIT,
|
|
72
|
+
MAX_DIRECT_SOURCE_ROWS,
|
|
73
|
+
resultPageMetadata,
|
|
74
|
+
takeBoundedItems,
|
|
75
|
+
} from './result-bounds';
|
|
69
76
|
|
|
70
77
|
export interface Env {
|
|
71
78
|
OAUTH_KV: KVNamespace;
|
|
@@ -108,7 +115,12 @@ const METRIC_OUTPUT_SCHEMA = {
|
|
|
108
115
|
};
|
|
109
116
|
|
|
110
117
|
const SEARCH_ROW_OUTPUT_SCHEMA = {
|
|
111
|
-
keys: z
|
|
118
|
+
keys: z
|
|
119
|
+
.array(z.string())
|
|
120
|
+
.optional()
|
|
121
|
+
.describe(
|
|
122
|
+
'Dimension values returned by Google. Aggregate queries with dimensions: [] may legitimately omit this field.',
|
|
123
|
+
),
|
|
112
124
|
...METRIC_OUTPUT_SCHEMA,
|
|
113
125
|
};
|
|
114
126
|
|
|
@@ -186,6 +198,9 @@ const SEARCH_ANALYTICS_OUTPUT_SCHEMA = {
|
|
|
186
198
|
start_row: z.number().int().nonnegative(),
|
|
187
199
|
rows: z.array(z.object(SEARCH_ROW_OUTPUT_SCHEMA)),
|
|
188
200
|
next_start_row: z.number().int().nonnegative().optional(),
|
|
201
|
+
has_more: z.boolean(),
|
|
202
|
+
truncated: z.boolean(),
|
|
203
|
+
byte_limit_reached: z.boolean(),
|
|
189
204
|
response_aggregation_type: z
|
|
190
205
|
.string()
|
|
191
206
|
.optional()
|
|
@@ -214,6 +229,34 @@ const SEARCH_ANALYTICS_PAGINATION_OUTPUT_SCHEMA = z.object({
|
|
|
214
229
|
local_limit_reached: z.boolean(),
|
|
215
230
|
});
|
|
216
231
|
|
|
232
|
+
const RESULT_PAGE_OUTPUT_SCHEMA = z.object({
|
|
233
|
+
start_row: z.number().int().nonnegative(),
|
|
234
|
+
limit: z.number().int().positive(),
|
|
235
|
+
returned_count: z.number().int().nonnegative(),
|
|
236
|
+
total_count: z.number().int().nonnegative().optional(),
|
|
237
|
+
has_more: z.boolean(),
|
|
238
|
+
truncated: z.boolean(),
|
|
239
|
+
byte_limit_reached: z.boolean(),
|
|
240
|
+
next_start_row: z.number().int().nonnegative().optional(),
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const ANALYSIS_RESULT_LIMIT_SCHEMA = z
|
|
244
|
+
.number()
|
|
245
|
+
.int()
|
|
246
|
+
.min(1)
|
|
247
|
+
.max(MAX_ANALYSIS_RESULT_LIMIT)
|
|
248
|
+
.default(DEFAULT_ANALYSIS_RESULT_LIMIT)
|
|
249
|
+
.describe(
|
|
250
|
+
`Maximum ranked results to return in this response (1-${MAX_ANALYSIS_RESULT_LIMIT}). Responses are also byte-bounded for MCP structured-content safety.`,
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
const RESULT_START_ROW_SCHEMA = z
|
|
254
|
+
.number()
|
|
255
|
+
.int()
|
|
256
|
+
.min(0)
|
|
257
|
+
.default(0)
|
|
258
|
+
.describe('Zero-based offset into the deterministically ranked result list.');
|
|
259
|
+
|
|
217
260
|
const QUICK_WIN_OUTPUT_SCHEMA = {
|
|
218
261
|
quick_wins: z.array(
|
|
219
262
|
z.object({
|
|
@@ -223,6 +266,7 @@ const QUICK_WIN_OUTPUT_SCHEMA = {
|
|
|
223
266
|
}),
|
|
224
267
|
),
|
|
225
268
|
pagination: SEARCH_ANALYTICS_PAGINATION_OUTPUT_SCHEMA,
|
|
269
|
+
result_page: RESULT_PAGE_OUTPUT_SCHEMA,
|
|
226
270
|
};
|
|
227
271
|
|
|
228
272
|
const PAGE_QUERIES_OUTPUT_SCHEMA = {
|
|
@@ -234,6 +278,7 @@ const PAGE_QUERIES_OUTPUT_SCHEMA = {
|
|
|
234
278
|
}),
|
|
235
279
|
),
|
|
236
280
|
pagination: SEARCH_ANALYTICS_PAGINATION_OUTPUT_SCHEMA,
|
|
281
|
+
result_page: RESULT_PAGE_OUTPUT_SCHEMA,
|
|
237
282
|
};
|
|
238
283
|
|
|
239
284
|
const QUERY_PAGES_OUTPUT_SCHEMA = {
|
|
@@ -245,6 +290,7 @@ const QUERY_PAGES_OUTPUT_SCHEMA = {
|
|
|
245
290
|
}),
|
|
246
291
|
),
|
|
247
292
|
pagination: SEARCH_ANALYTICS_PAGINATION_OUTPUT_SCHEMA,
|
|
293
|
+
result_page: RESULT_PAGE_OUTPUT_SCHEMA,
|
|
248
294
|
};
|
|
249
295
|
|
|
250
296
|
const CANNIBALIZATION_OUTPUT_SCHEMA = {
|
|
@@ -253,6 +299,8 @@ const CANNIBALIZATION_OUTPUT_SCHEMA = {
|
|
|
253
299
|
query: z.string(),
|
|
254
300
|
total_clicks: z.number(),
|
|
255
301
|
total_impressions: z.number(),
|
|
302
|
+
page_count: z.number().int().nonnegative(),
|
|
303
|
+
pages_truncated: z.boolean(),
|
|
256
304
|
pages: z.array(
|
|
257
305
|
z.object({
|
|
258
306
|
page: z.string(),
|
|
@@ -263,6 +311,7 @@ const CANNIBALIZATION_OUTPUT_SCHEMA = {
|
|
|
263
311
|
}),
|
|
264
312
|
),
|
|
265
313
|
pagination: SEARCH_ANALYTICS_PAGINATION_OUTPUT_SCHEMA,
|
|
314
|
+
result_page: RESULT_PAGE_OUTPUT_SCHEMA,
|
|
266
315
|
};
|
|
267
316
|
|
|
268
317
|
const CONTENT_DECAY_OUTPUT_SCHEMA = {
|
|
@@ -271,9 +320,16 @@ const CONTENT_DECAY_OUTPUT_SCHEMA = {
|
|
|
271
320
|
previous: z.object({ start: z.string(), end: z.string() }),
|
|
272
321
|
}),
|
|
273
322
|
decay_count: z.number().int().nonnegative(),
|
|
323
|
+
assessment_count: z.number().int().nonnegative(),
|
|
274
324
|
decay_results: z.array(
|
|
275
325
|
z.object({
|
|
276
326
|
page: z.string(),
|
|
327
|
+
classification: z.enum([
|
|
328
|
+
'likely_decay',
|
|
329
|
+
'weak_insufficient_evidence',
|
|
330
|
+
'improving_visibility_with_click_volatility',
|
|
331
|
+
]),
|
|
332
|
+
evidence: z.string(),
|
|
277
333
|
previous_clicks: z.number(),
|
|
278
334
|
recent_clicks: z.number(),
|
|
279
335
|
click_difference: z.number(),
|
|
@@ -281,14 +337,17 @@ const CONTENT_DECAY_OUTPUT_SCHEMA = {
|
|
|
281
337
|
previous_impressions: z.number(),
|
|
282
338
|
recent_impressions: z.number(),
|
|
283
339
|
impression_difference: z.number(),
|
|
340
|
+
impression_change_percentage: z.number().nullable(),
|
|
284
341
|
previous_position: z.number(),
|
|
285
342
|
recent_position: z.number(),
|
|
343
|
+
position_change: z.number().nullable(),
|
|
286
344
|
}),
|
|
287
345
|
),
|
|
288
346
|
pagination: z.object({
|
|
289
347
|
recent: SEARCH_ANALYTICS_PAGINATION_OUTPUT_SCHEMA,
|
|
290
348
|
previous: SEARCH_ANALYTICS_PAGINATION_OUTPUT_SCHEMA,
|
|
291
349
|
}),
|
|
350
|
+
result_page: RESULT_PAGE_OUTPUT_SCHEMA,
|
|
292
351
|
};
|
|
293
352
|
|
|
294
353
|
const INDEXING_OUTPUT_SCHEMA = {
|
|
@@ -298,8 +357,16 @@ const INDEXING_OUTPUT_SCHEMA = {
|
|
|
298
357
|
|
|
299
358
|
const INDEXED_PAGES_OUTPUT_SCHEMA = {
|
|
300
359
|
pages: z.array(z.object({ page: z.string(), ...METRIC_OUTPUT_SCHEMA })),
|
|
360
|
+
note: z.string(),
|
|
361
|
+
result_page: RESULT_PAGE_OUTPUT_SCHEMA,
|
|
301
362
|
};
|
|
302
363
|
|
|
364
|
+
const SEARCH_VISIBLE_PAGES_DESCRIPTION =
|
|
365
|
+
'List pages that produced Search Console impressions in the requested period. This is performance data, not an index-coverage inventory: a URL can be indexed even when it is absent here, and Search Analytics can omit some page-level detail. Use urls.inspect or urls.inspect_many for URL-level index status.';
|
|
366
|
+
|
|
367
|
+
const SEARCH_VISIBLE_PAGES_NOTE =
|
|
368
|
+
'These are Search Analytics performance rows for pages with recorded impressions in the requested period, not a complete list of indexed URLs. Absence does not mean a URL is unindexed. Use urls.inspect or urls.inspect_many for URL-level index status.';
|
|
369
|
+
|
|
303
370
|
const PERFORMANCE_COMPARISON_OUTPUT_SCHEMA = {
|
|
304
371
|
comparisons: z.array(
|
|
305
372
|
z.object({
|
|
@@ -326,6 +393,7 @@ const PERFORMANCE_COMPARISON_OUTPUT_SCHEMA = {
|
|
|
326
393
|
period_a: SEARCH_ANALYTICS_PAGINATION_OUTPUT_SCHEMA,
|
|
327
394
|
period_b: SEARCH_ANALYTICS_PAGINATION_OUTPUT_SCHEMA,
|
|
328
395
|
}),
|
|
396
|
+
result_page: RESULT_PAGE_OUTPUT_SCHEMA,
|
|
329
397
|
};
|
|
330
398
|
|
|
331
399
|
const WEEKLY_DIGEST_OUTPUT_SCHEMA = {
|
|
@@ -350,6 +418,53 @@ function paginationMetadata(result: PaginatedSearchAnalyticsResult) {
|
|
|
350
418
|
};
|
|
351
419
|
}
|
|
352
420
|
|
|
421
|
+
function windowKnownResults<T>(
|
|
422
|
+
items: readonly T[],
|
|
423
|
+
startRow: number,
|
|
424
|
+
limit: number,
|
|
425
|
+
) {
|
|
426
|
+
const requested = items.slice(startRow, startRow + limit);
|
|
427
|
+
const bounded = takeBoundedItems(requested, limit);
|
|
428
|
+
const returnedCount = bounded.items.length;
|
|
429
|
+
const hasMore =
|
|
430
|
+
startRow + returnedCount < items.length || bounded.byteLimitReached;
|
|
431
|
+
return {
|
|
432
|
+
items: bounded.items,
|
|
433
|
+
resultPage: resultPageMetadata({
|
|
434
|
+
startRow,
|
|
435
|
+
limit,
|
|
436
|
+
returnedCount,
|
|
437
|
+
totalCount: items.length,
|
|
438
|
+
hasMore,
|
|
439
|
+
byteLimitReached: bounded.byteLimitReached,
|
|
440
|
+
}),
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function windowSourceRows<T>(
|
|
445
|
+
items: readonly T[],
|
|
446
|
+
sourceStartRow: number,
|
|
447
|
+
requestedLimit: number,
|
|
448
|
+
sourceMayHaveMore: boolean,
|
|
449
|
+
) {
|
|
450
|
+
const bounded = takeBoundedItems(items, requestedLimit);
|
|
451
|
+
const returnedCount = bounded.items.length;
|
|
452
|
+
const hasMore =
|
|
453
|
+
bounded.byteLimitReached ||
|
|
454
|
+
returnedCount < items.length ||
|
|
455
|
+
sourceMayHaveMore;
|
|
456
|
+
return {
|
|
457
|
+
items: bounded.items,
|
|
458
|
+
resultPage: resultPageMetadata({
|
|
459
|
+
startRow: sourceStartRow,
|
|
460
|
+
limit: requestedLimit,
|
|
461
|
+
returnedCount,
|
|
462
|
+
hasMore,
|
|
463
|
+
byteLimitReached: bounded.byteLimitReached,
|
|
464
|
+
}),
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
353
468
|
function toolError(message: string) {
|
|
354
469
|
return {
|
|
355
470
|
content: [{ type: 'text' as const, text: message }],
|
|
@@ -438,7 +553,7 @@ const TOOL_CATALOG = [
|
|
|
438
553
|
{
|
|
439
554
|
name: 'insights.content_decay',
|
|
440
555
|
description:
|
|
441
|
-
'
|
|
556
|
+
'Assess page-level click declines across two contiguous periods and distinguish likely decay from weak evidence or improving visibility with click volatility.',
|
|
442
557
|
},
|
|
443
558
|
{
|
|
444
559
|
name: 'indexing.request',
|
|
@@ -447,8 +562,7 @@ const TOOL_CATALOG = [
|
|
|
447
562
|
},
|
|
448
563
|
{
|
|
449
564
|
name: 'indexing.list_pages',
|
|
450
|
-
description:
|
|
451
|
-
'Retrieve a list of site pages that have received search impressions, serving as a proxy list of indexed pages on the site. When one date boundary is omitted, the server derives the other to target an inclusive 30-day range; generated end dates are capped at the latest complete date.',
|
|
565
|
+
description: SEARCH_VISIBLE_PAGES_DESCRIPTION,
|
|
452
566
|
},
|
|
453
567
|
{
|
|
454
568
|
name: 'analytics.compare',
|
|
@@ -724,10 +838,14 @@ class GscMcpRuntime {
|
|
|
724
838
|
title: 'Query search analytics',
|
|
725
839
|
description: [
|
|
726
840
|
'Query Google Search Console search analytics data. Returns',
|
|
727
|
-
'{ row_count, start_row, rows
|
|
728
|
-
'
|
|
729
|
-
'
|
|
730
|
-
'
|
|
841
|
+
'{ row_count, start_row, rows, has_more, truncated, byte_limit_reached }',
|
|
842
|
+
'where dimensioned rows have keys plus clicks, impressions, ctr, and',
|
|
843
|
+
'position. Aggregate rows from dimensions: [] may omit keys because',
|
|
844
|
+
'Google itself omits that field. When has_more is true, the response',
|
|
845
|
+
'includes next_start_row — pass it back as start_row to fetch the next',
|
|
846
|
+
'safe page. Large requested row_limit values are automatically split',
|
|
847
|
+
'into bounded MCP responses rather than failing structured output.',
|
|
848
|
+
'When Google provides',
|
|
731
849
|
'them, response_aggregation_type and metadata are also included;',
|
|
732
850
|
'metadata may identify the first incomplete date or hour.',
|
|
733
851
|
'',
|
|
@@ -797,7 +915,7 @@ class GscMcpRuntime {
|
|
|
797
915
|
.max(25000)
|
|
798
916
|
.default(100)
|
|
799
917
|
.describe(
|
|
800
|
-
|
|
918
|
+
`Maximum rows requested for this logical page (1-25000). Defaults to 100. MCP responses are additionally capped to at most ${MAX_DIRECT_SOURCE_ROWS} source rows and a structured-content byte budget; use next_start_row while has_more is true for bulk exports.`,
|
|
801
919
|
),
|
|
802
920
|
start_row: z
|
|
803
921
|
.number()
|
|
@@ -874,11 +992,12 @@ class GscMcpRuntime {
|
|
|
874
992
|
const rateLimitError = await this.rateLimitError(googleId, 'analytics.query');
|
|
875
993
|
if (rateLimitError) return rateLimitError;
|
|
876
994
|
const accessToken = await this.getAccessToken(googleId);
|
|
995
|
+
const sourceRowLimit = Math.min(row_limit, MAX_DIRECT_SOURCE_ROWS);
|
|
877
996
|
const response = await querySearchAnalytics(accessToken, site_url, {
|
|
878
997
|
startDate: start_date,
|
|
879
998
|
endDate: end_date,
|
|
880
999
|
dimensions,
|
|
881
|
-
rowLimit:
|
|
1000
|
+
rowLimit: sourceRowLimit,
|
|
882
1001
|
startRow: start_row,
|
|
883
1002
|
dataState: data_state,
|
|
884
1003
|
type: search_type,
|
|
@@ -887,11 +1006,21 @@ class GscMcpRuntime {
|
|
|
887
1006
|
? { dimensionFilterGroups: dimension_filter_groups }
|
|
888
1007
|
: {}),
|
|
889
1008
|
});
|
|
890
|
-
const
|
|
1009
|
+
const sourceMayHaveMore =
|
|
1010
|
+
dimensions.length > 0 && response.rows.length === sourceRowLimit;
|
|
1011
|
+
const bounded = windowSourceRows(
|
|
1012
|
+
response.rows,
|
|
1013
|
+
start_row,
|
|
1014
|
+
row_limit,
|
|
1015
|
+
sourceMayHaveMore,
|
|
1016
|
+
);
|
|
891
1017
|
const payload: Record<string, unknown> = {
|
|
892
|
-
row_count:
|
|
1018
|
+
row_count: bounded.items.length,
|
|
893
1019
|
start_row,
|
|
894
|
-
rows,
|
|
1020
|
+
rows: bounded.items,
|
|
1021
|
+
has_more: bounded.resultPage.has_more,
|
|
1022
|
+
truncated: bounded.resultPage.truncated,
|
|
1023
|
+
byte_limit_reached: bounded.resultPage.byte_limit_reached,
|
|
895
1024
|
};
|
|
896
1025
|
if (response.responseAggregationType !== undefined) {
|
|
897
1026
|
payload.response_aggregation_type = response.responseAggregationType;
|
|
@@ -899,8 +1028,8 @@ class GscMcpRuntime {
|
|
|
899
1028
|
if (response.metadata !== undefined) {
|
|
900
1029
|
payload.metadata = response.metadata;
|
|
901
1030
|
}
|
|
902
|
-
if (
|
|
903
|
-
payload.next_start_row =
|
|
1031
|
+
if (bounded.resultPage.next_start_row !== undefined) {
|
|
1032
|
+
payload.next_start_row = bounded.resultPage.next_start_row;
|
|
904
1033
|
}
|
|
905
1034
|
// Compact JSON on purpose: analytics responses are the largest this
|
|
906
1035
|
// server produces, and pretty-printing them costs ~3x the tokens.
|
|
@@ -912,7 +1041,7 @@ class GscMcpRuntime {
|
|
|
912
1041
|
'insights.page_queries',
|
|
913
1042
|
{
|
|
914
1043
|
title: 'Find queries for a page',
|
|
915
|
-
description: 'For one exact page URL, return the Search Console queries that produced impressions for it over a date range. This wraps an exact page dimension filter so agents do not need to construct analytics.query filter groups manually. Exact page matching is case-sensitive in Search Console.
|
|
1044
|
+
description: 'For one exact page URL, return the Search Console queries that produced impressions for it over a date range. This wraps an exact page dimension filter so agents do not need to construct analytics.query filter groups manually. Exact page matching is case-sensitive in Search Console. Use row_limit and start_row to page through bounded results; Search Console can still omit anonymized queries.',
|
|
916
1045
|
inputSchema: {
|
|
917
1046
|
site_url: z.string().describe(SITE_URL_DESCRIPTION),
|
|
918
1047
|
page_url: z.string().url().describe('Exact fully-qualified page URL to filter on, e.g. https://example.com/guides/seo/. Search Console exact page filters are case-sensitive.'),
|
|
@@ -922,20 +1051,35 @@ class GscMcpRuntime {
|
|
|
922
1051
|
.enum(['web', 'image', 'video', 'news', 'discover', 'googleNews'])
|
|
923
1052
|
.default('web')
|
|
924
1053
|
.describe('Which search index to query. Defaults to web.'),
|
|
1054
|
+
row_limit: z
|
|
1055
|
+
.number()
|
|
1056
|
+
.int()
|
|
1057
|
+
.min(1)
|
|
1058
|
+
.max(25000)
|
|
1059
|
+
.default(100)
|
|
1060
|
+
.describe('Maximum query rows requested for this response. Continue with result_page.next_start_row while result_page.has_more is true.'),
|
|
1061
|
+
start_row: z
|
|
1062
|
+
.number()
|
|
1063
|
+
.int()
|
|
1064
|
+
.min(0)
|
|
1065
|
+
.default(0)
|
|
1066
|
+
.describe('Zero-based Search Analytics row offset for pagination.'),
|
|
925
1067
|
},
|
|
926
1068
|
outputSchema: PAGE_QUERIES_OUTPUT_SCHEMA,
|
|
927
1069
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
928
1070
|
},
|
|
929
|
-
async ({ site_url, page_url, start_date, end_date, search_type }) => {
|
|
1071
|
+
async ({ site_url, page_url, start_date, end_date, search_type, row_limit = 100, start_row = 0 }) => {
|
|
930
1072
|
assertDateRange(start_date, end_date);
|
|
931
1073
|
const googleId = this.requireGoogleId();
|
|
932
1074
|
const rateLimitError = await this.rateLimitError(googleId, 'insights.page_queries');
|
|
933
1075
|
if (rateLimitError) return rateLimitError;
|
|
934
1076
|
const accessToken = await this.getAccessToken(googleId);
|
|
1077
|
+
const sourceRowLimit = Math.min(row_limit, MAX_DIRECT_SOURCE_ROWS);
|
|
935
1078
|
const source = await querySearchAnalyticsPaginated(accessToken, site_url, {
|
|
936
1079
|
startDate: start_date,
|
|
937
1080
|
endDate: end_date,
|
|
938
1081
|
dimensions: ['query'],
|
|
1082
|
+
startRow: start_row,
|
|
939
1083
|
type: search_type,
|
|
940
1084
|
dimensionFilterGroups: [
|
|
941
1085
|
{
|
|
@@ -945,21 +1089,29 @@ class GscMcpRuntime {
|
|
|
945
1089
|
],
|
|
946
1090
|
},
|
|
947
1091
|
],
|
|
948
|
-
});
|
|
1092
|
+
}, { maxRows: sourceRowLimit });
|
|
1093
|
+
const rows = source.rows
|
|
1094
|
+
.filter((row) => (row.keys?.length ?? 0) > 0)
|
|
1095
|
+
.map((row) => ({
|
|
1096
|
+
query: row.keys![0],
|
|
1097
|
+
clicks: row.clicks,
|
|
1098
|
+
impressions: row.impressions,
|
|
1099
|
+
ctr: row.ctr,
|
|
1100
|
+
position: row.position,
|
|
1101
|
+
}));
|
|
1102
|
+
const bounded = windowSourceRows(
|
|
1103
|
+
rows,
|
|
1104
|
+
start_row,
|
|
1105
|
+
row_limit,
|
|
1106
|
+
source.localLimitReached,
|
|
1107
|
+
);
|
|
949
1108
|
const payload = {
|
|
950
1109
|
page: page_url,
|
|
951
|
-
queries:
|
|
952
|
-
.filter((row) => row.keys.length > 0)
|
|
953
|
-
.map((row) => ({
|
|
954
|
-
query: row.keys[0],
|
|
955
|
-
clicks: row.clicks,
|
|
956
|
-
impressions: row.impressions,
|
|
957
|
-
ctr: row.ctr,
|
|
958
|
-
position: row.position,
|
|
959
|
-
})),
|
|
1110
|
+
queries: bounded.items,
|
|
960
1111
|
pagination: paginationMetadata(source),
|
|
1112
|
+
result_page: bounded.resultPage,
|
|
961
1113
|
};
|
|
962
|
-
return toolResponse(JSON.stringify(payload
|
|
1114
|
+
return toolResponse(JSON.stringify(payload), payload);
|
|
963
1115
|
},
|
|
964
1116
|
);
|
|
965
1117
|
|
|
@@ -967,7 +1119,7 @@ class GscMcpRuntime {
|
|
|
967
1119
|
'insights.query_pages',
|
|
968
1120
|
{
|
|
969
1121
|
title: 'Find pages for a query',
|
|
970
|
-
description: 'For one exact search query, return the site pages that received impressions for it over a date range. This wraps an exact query dimension filter so agents do not need to construct analytics.query filter groups manually. Exact query matching is case-sensitive in Search Console. Use
|
|
1122
|
+
description: 'For one exact search query, return the site pages that received impressions for it over a date range. This wraps an exact query dimension filter so agents do not need to construct analytics.query filter groups manually. Exact query matching is case-sensitive in Search Console. Use row_limit and start_row to page through bounded results and verify which URL Google is surfacing before diagnosing cannibalization or content targeting.',
|
|
971
1123
|
inputSchema: {
|
|
972
1124
|
site_url: z.string().describe(SITE_URL_DESCRIPTION),
|
|
973
1125
|
query: z.string().min(1).describe('Exact Search Console query text to filter on. Exact query filters are case-sensitive.'),
|
|
@@ -977,20 +1129,35 @@ class GscMcpRuntime {
|
|
|
977
1129
|
.enum(['web', 'image', 'video', 'news', 'discover', 'googleNews'])
|
|
978
1130
|
.default('web')
|
|
979
1131
|
.describe('Which search index to query. Defaults to web.'),
|
|
1132
|
+
row_limit: z
|
|
1133
|
+
.number()
|
|
1134
|
+
.int()
|
|
1135
|
+
.min(1)
|
|
1136
|
+
.max(25000)
|
|
1137
|
+
.default(100)
|
|
1138
|
+
.describe('Maximum page rows requested for this response. Continue with result_page.next_start_row while result_page.has_more is true.'),
|
|
1139
|
+
start_row: z
|
|
1140
|
+
.number()
|
|
1141
|
+
.int()
|
|
1142
|
+
.min(0)
|
|
1143
|
+
.default(0)
|
|
1144
|
+
.describe('Zero-based Search Analytics row offset for pagination.'),
|
|
980
1145
|
},
|
|
981
1146
|
outputSchema: QUERY_PAGES_OUTPUT_SCHEMA,
|
|
982
1147
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
983
1148
|
},
|
|
984
|
-
async ({ site_url, query, start_date, end_date, search_type }) => {
|
|
1149
|
+
async ({ site_url, query, start_date, end_date, search_type, row_limit = 100, start_row = 0 }) => {
|
|
985
1150
|
assertDateRange(start_date, end_date);
|
|
986
1151
|
const googleId = this.requireGoogleId();
|
|
987
1152
|
const rateLimitError = await this.rateLimitError(googleId, 'insights.query_pages');
|
|
988
1153
|
if (rateLimitError) return rateLimitError;
|
|
989
1154
|
const accessToken = await this.getAccessToken(googleId);
|
|
1155
|
+
const sourceRowLimit = Math.min(row_limit, MAX_DIRECT_SOURCE_ROWS);
|
|
990
1156
|
const source = await querySearchAnalyticsPaginated(accessToken, site_url, {
|
|
991
1157
|
startDate: start_date,
|
|
992
1158
|
endDate: end_date,
|
|
993
1159
|
dimensions: ['page'],
|
|
1160
|
+
startRow: start_row,
|
|
994
1161
|
type: search_type,
|
|
995
1162
|
dimensionFilterGroups: [
|
|
996
1163
|
{
|
|
@@ -1000,21 +1167,29 @@ class GscMcpRuntime {
|
|
|
1000
1167
|
],
|
|
1001
1168
|
},
|
|
1002
1169
|
],
|
|
1003
|
-
});
|
|
1170
|
+
}, { maxRows: sourceRowLimit });
|
|
1171
|
+
const rows = source.rows
|
|
1172
|
+
.filter((row) => (row.keys?.length ?? 0) > 0)
|
|
1173
|
+
.map((row) => ({
|
|
1174
|
+
page: row.keys![0],
|
|
1175
|
+
clicks: row.clicks,
|
|
1176
|
+
impressions: row.impressions,
|
|
1177
|
+
ctr: row.ctr,
|
|
1178
|
+
position: row.position,
|
|
1179
|
+
}));
|
|
1180
|
+
const bounded = windowSourceRows(
|
|
1181
|
+
rows,
|
|
1182
|
+
start_row,
|
|
1183
|
+
row_limit,
|
|
1184
|
+
source.localLimitReached,
|
|
1185
|
+
);
|
|
1004
1186
|
const payload = {
|
|
1005
1187
|
query,
|
|
1006
|
-
pages:
|
|
1007
|
-
.filter((row) => row.keys.length > 0)
|
|
1008
|
-
.map((row) => ({
|
|
1009
|
-
page: row.keys[0],
|
|
1010
|
-
clicks: row.clicks,
|
|
1011
|
-
impressions: row.impressions,
|
|
1012
|
-
ctr: row.ctr,
|
|
1013
|
-
position: row.position,
|
|
1014
|
-
})),
|
|
1188
|
+
pages: bounded.items,
|
|
1015
1189
|
pagination: paginationMetadata(source),
|
|
1190
|
+
result_page: bounded.resultPage,
|
|
1016
1191
|
};
|
|
1017
|
-
return toolResponse(JSON.stringify(payload
|
|
1192
|
+
return toolResponse(JSON.stringify(payload), payload);
|
|
1018
1193
|
},
|
|
1019
1194
|
);
|
|
1020
1195
|
|
|
@@ -1134,12 +1309,12 @@ class GscMcpRuntime {
|
|
|
1134
1309
|
'insights.quick_wins',
|
|
1135
1310
|
{
|
|
1136
1311
|
title: 'Identify SEO Quick Wins',
|
|
1137
|
-
description: 'Find search queries with at least the requested impressions that rank in a configurable striking-distance position range (8-20 by default). Returns clicks, impressions, CTR, and average position; CTR is context, not an eligibility filter.
|
|
1312
|
+
description: 'Find search queries with at least the requested impressions that rank in a configurable striking-distance position range (8-20 by default). Returns clicks, impressions, CTR, and average position; CTR is context, not an eligibility filter. Results are ranked deterministically by impressions, then bounded with limit/start_row and explicit result_page metadata. Source pagination separately flags the local 100,000-row safety ceiling.',
|
|
1138
1313
|
inputSchema: createQuickWinsInputSchema(SITE_URL_DESCRIPTION),
|
|
1139
1314
|
outputSchema: z.object(QUICK_WIN_OUTPUT_SCHEMA),
|
|
1140
1315
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
1141
1316
|
},
|
|
1142
|
-
async ({ site_url, start_date, end_date, min_impressions, min_position, max_position }) => {
|
|
1317
|
+
async ({ site_url, start_date, end_date, min_impressions, min_position, max_position, limit = DEFAULT_ANALYSIS_RESULT_LIMIT, start_row = 0 }) => {
|
|
1143
1318
|
assertDateRange(start_date, end_date);
|
|
1144
1319
|
const googleId = this.requireGoogleId();
|
|
1145
1320
|
const rateLimitError = await this.rateLimitError(googleId, 'insights.quick_wins');
|
|
@@ -1152,11 +1327,13 @@ class GscMcpRuntime {
|
|
|
1152
1327
|
});
|
|
1153
1328
|
|
|
1154
1329
|
const quickWins = processQuickWins(source.rows, min_impressions, min_position, max_position);
|
|
1330
|
+
const bounded = windowKnownResults(quickWins, start_row, limit);
|
|
1155
1331
|
const payload = {
|
|
1156
|
-
quick_wins:
|
|
1332
|
+
quick_wins: bounded.items,
|
|
1157
1333
|
pagination: paginationMetadata(source),
|
|
1334
|
+
result_page: bounded.resultPage,
|
|
1158
1335
|
};
|
|
1159
|
-
return toolResponse(JSON.stringify(payload
|
|
1336
|
+
return toolResponse(JSON.stringify(payload), payload);
|
|
1160
1337
|
},
|
|
1161
1338
|
);
|
|
1162
1339
|
|
|
@@ -1164,18 +1341,20 @@ class GscMcpRuntime {
|
|
|
1164
1341
|
'insights.cannibalization',
|
|
1165
1342
|
{
|
|
1166
1343
|
title: 'Detect Keyword Cannibalization',
|
|
1167
|
-
description: 'Analyze
|
|
1344
|
+
description: 'Analyze query/page Search Analytics to find queries split across multiple pages. Candidates are ranked deterministically by total impressions, each candidate bounds its page list, and limit/start_row plus result_page provide safe pagination. Multiple ranking URLs can also reflect legitimate locale or intent differences, so treat candidates as evidence to investigate rather than proof of harmful cannibalization.',
|
|
1168
1345
|
inputSchema: {
|
|
1169
1346
|
site_url: z.string().describe(SITE_URL_DESCRIPTION),
|
|
1170
1347
|
start_date: SEARCH_CONSOLE_DATE_SCHEMA.describe('Start date (inclusive) in YYYY-MM-DD format.'),
|
|
1171
1348
|
end_date: SEARCH_CONSOLE_DATE_SCHEMA.describe('End date (inclusive) in YYYY-MM-DD format. Note the 2-3 day GSC data lag.'),
|
|
1172
1349
|
min_impressions: CANNIBALIZATION_MIN_IMPRESSIONS_SCHEMA,
|
|
1173
1350
|
min_page_percentage: CANNIBALIZATION_MIN_PAGE_PERCENTAGE_SCHEMA,
|
|
1351
|
+
limit: ANALYSIS_RESULT_LIMIT_SCHEMA,
|
|
1352
|
+
start_row: RESULT_START_ROW_SCHEMA,
|
|
1174
1353
|
},
|
|
1175
1354
|
outputSchema: CANNIBALIZATION_OUTPUT_SCHEMA,
|
|
1176
1355
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
1177
1356
|
},
|
|
1178
|
-
async ({ site_url, start_date, end_date, min_impressions, min_page_percentage }) => {
|
|
1357
|
+
async ({ site_url, start_date, end_date, min_impressions, min_page_percentage, limit = DEFAULT_ANALYSIS_RESULT_LIMIT, start_row = 0 }) => {
|
|
1179
1358
|
assertDateRange(start_date, end_date);
|
|
1180
1359
|
const googleId = this.requireGoogleId();
|
|
1181
1360
|
const rateLimitError = await this.rateLimitError(googleId, 'insights.cannibalization');
|
|
@@ -1187,12 +1366,32 @@ class GscMcpRuntime {
|
|
|
1187
1366
|
dimensions: ['query', 'page'],
|
|
1188
1367
|
});
|
|
1189
1368
|
|
|
1190
|
-
const cannibalizationCandidates = processCannibalization(
|
|
1369
|
+
const cannibalizationCandidates = processCannibalization(
|
|
1370
|
+
source.rows,
|
|
1371
|
+
min_impressions,
|
|
1372
|
+
min_page_percentage,
|
|
1373
|
+
).map((candidate) => {
|
|
1374
|
+
const boundedPages = takeBoundedItems(candidate.pages, 10, 12_000);
|
|
1375
|
+
return {
|
|
1376
|
+
...candidate,
|
|
1377
|
+
page_count: candidate.pages.length,
|
|
1378
|
+
pages_truncated:
|
|
1379
|
+
boundedPages.byteLimitReached ||
|
|
1380
|
+
boundedPages.items.length < candidate.pages.length,
|
|
1381
|
+
pages: boundedPages.items,
|
|
1382
|
+
};
|
|
1383
|
+
});
|
|
1384
|
+
const bounded = windowKnownResults(
|
|
1385
|
+
cannibalizationCandidates,
|
|
1386
|
+
start_row,
|
|
1387
|
+
limit,
|
|
1388
|
+
);
|
|
1191
1389
|
const payload = {
|
|
1192
|
-
candidates:
|
|
1390
|
+
candidates: bounded.items,
|
|
1193
1391
|
pagination: paginationMetadata(source),
|
|
1392
|
+
result_page: bounded.resultPage,
|
|
1194
1393
|
};
|
|
1195
|
-
return toolResponse(JSON.stringify(payload
|
|
1394
|
+
return toolResponse(JSON.stringify(payload), payload);
|
|
1196
1395
|
},
|
|
1197
1396
|
);
|
|
1198
1397
|
|
|
@@ -1200,15 +1399,17 @@ class GscMcpRuntime {
|
|
|
1200
1399
|
'insights.content_decay',
|
|
1201
1400
|
{
|
|
1202
1401
|
title: 'Detect Content Decay',
|
|
1203
|
-
description: '
|
|
1402
|
+
description: 'Assess page-level click declines across two contiguous periods without treating every small click change as content decay. Each result is classified as likely_decay, weak_insufficient_evidence, or improving_visibility_with_click_volatility using deterministic click-volume, impression, and average-position signals. This is a heuristic assessment, not statistical proof. Results are bounded with limit/start_row; source pagination is reported separately.',
|
|
1204
1403
|
inputSchema: {
|
|
1205
1404
|
site_url: z.string().describe(SITE_URL_DESCRIPTION),
|
|
1206
1405
|
compare_days: CONTENT_DECAY_COMPARE_DAYS_SCHEMA,
|
|
1406
|
+
limit: ANALYSIS_RESULT_LIMIT_SCHEMA,
|
|
1407
|
+
start_row: RESULT_START_ROW_SCHEMA,
|
|
1207
1408
|
},
|
|
1208
1409
|
outputSchema: CONTENT_DECAY_OUTPUT_SCHEMA,
|
|
1209
1410
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
1210
1411
|
},
|
|
1211
|
-
async ({ site_url, compare_days }) => {
|
|
1412
|
+
async ({ site_url, compare_days, limit = DEFAULT_ANALYSIS_RESULT_LIMIT, start_row = 0 }) => {
|
|
1212
1413
|
const googleId = this.requireGoogleId();
|
|
1213
1414
|
const rateLimitError = await this.rateLimitError(googleId, 'insights.content_decay');
|
|
1214
1415
|
if (rateLimitError) return rateLimitError;
|
|
@@ -1241,21 +1442,29 @@ class GscMcpRuntime {
|
|
|
1241
1442
|
dimensions: ['page'],
|
|
1242
1443
|
});
|
|
1243
1444
|
|
|
1244
|
-
const decayResults = processContentDecay(
|
|
1445
|
+
const decayResults = processContentDecay(
|
|
1446
|
+
recentSource.rows,
|
|
1447
|
+
previousSource.rows,
|
|
1448
|
+
);
|
|
1449
|
+
const bounded = windowKnownResults(decayResults, start_row, limit);
|
|
1245
1450
|
|
|
1246
1451
|
const payload = {
|
|
1247
1452
|
comparison_periods: {
|
|
1248
1453
|
recent: { start: recentStart, end: recentEnd },
|
|
1249
1454
|
previous: { start: previousStart, end: previousEnd },
|
|
1250
1455
|
},
|
|
1251
|
-
decay_count: decayResults.
|
|
1252
|
-
|
|
1456
|
+
decay_count: decayResults.filter(
|
|
1457
|
+
(result) => result.classification === 'likely_decay',
|
|
1458
|
+
).length,
|
|
1459
|
+
assessment_count: decayResults.length,
|
|
1460
|
+
decay_results: bounded.items,
|
|
1253
1461
|
pagination: {
|
|
1254
1462
|
recent: paginationMetadata(recentSource),
|
|
1255
1463
|
previous: paginationMetadata(previousSource),
|
|
1256
1464
|
},
|
|
1465
|
+
result_page: bounded.resultPage,
|
|
1257
1466
|
};
|
|
1258
|
-
return toolResponse(JSON.stringify(payload
|
|
1467
|
+
return toolResponse(JSON.stringify(payload), payload);
|
|
1259
1468
|
},
|
|
1260
1469
|
);
|
|
1261
1470
|
|
|
@@ -1301,18 +1510,19 @@ class GscMcpRuntime {
|
|
|
1301
1510
|
this.server.registerTool(
|
|
1302
1511
|
'indexing.list_pages',
|
|
1303
1512
|
{
|
|
1304
|
-
title: 'List
|
|
1305
|
-
description:
|
|
1513
|
+
title: 'List Search-Visible Pages',
|
|
1514
|
+
description: `${SEARCH_VISIBLE_PAGES_DESCRIPTION} When one date boundary is omitted, the server derives the other to target an inclusive 30-day range; generated end dates are capped at the latest complete date. Responses are bounded and pageable with row_limit/start_row.`,
|
|
1306
1515
|
inputSchema: {
|
|
1307
1516
|
site_url: z.string().describe(SITE_URL_DESCRIPTION),
|
|
1308
1517
|
start_date: SEARCH_CONSOLE_DATE_SCHEMA.optional().describe('Start date (inclusive) in YYYY-MM-DD format. If end_date is omitted, the generated end date is 29 days later, capped at the latest complete date.'),
|
|
1309
1518
|
end_date: SEARCH_CONSOLE_DATE_SCHEMA.optional().describe('End date (inclusive) in YYYY-MM-DD format. If start_date is omitted, the generated start date is 29 days earlier. Defaults to 3 days ago. Note the 2-3 day data lag.'),
|
|
1310
|
-
row_limit: z.number().int().min(1).max(25000).default(1000).describe(
|
|
1519
|
+
row_limit: z.number().int().min(1).max(25000).default(1000).describe(`Maximum pages requested for this logical response (1-25000). Output is safely bounded; continue with result_page.next_start_row while result_page.has_more is true.`),
|
|
1520
|
+
start_row: z.number().int().min(0).default(0).describe('Zero-based page-row offset for pagination.'),
|
|
1311
1521
|
},
|
|
1312
1522
|
outputSchema: INDEXED_PAGES_OUTPUT_SCHEMA,
|
|
1313
1523
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
1314
1524
|
},
|
|
1315
|
-
async ({ site_url, start_date, end_date, row_limit }) => {
|
|
1525
|
+
async ({ site_url, start_date, end_date, row_limit, start_row = 0 }) => {
|
|
1316
1526
|
const { startDate, endDate } = resolveIndexedPagesDateRange(
|
|
1317
1527
|
start_date,
|
|
1318
1528
|
end_date,
|
|
@@ -1323,22 +1533,37 @@ class GscMcpRuntime {
|
|
|
1323
1533
|
if (rateLimitError) return rateLimitError;
|
|
1324
1534
|
const accessToken = await this.getAccessToken(googleId);
|
|
1325
1535
|
|
|
1536
|
+
const sourceRowLimit = Math.min(row_limit, MAX_DIRECT_SOURCE_ROWS);
|
|
1326
1537
|
const response = await querySearchAnalytics(accessToken, site_url, {
|
|
1327
1538
|
startDate,
|
|
1328
1539
|
endDate,
|
|
1329
1540
|
dimensions: ['page'],
|
|
1330
|
-
rowLimit:
|
|
1541
|
+
rowLimit: sourceRowLimit,
|
|
1542
|
+
startRow: start_row,
|
|
1331
1543
|
});
|
|
1332
1544
|
|
|
1333
|
-
const pages = response.rows
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1545
|
+
const pages = response.rows
|
|
1546
|
+
.filter((row) => (row.keys?.length ?? 0) > 0)
|
|
1547
|
+
.map((row) => ({
|
|
1548
|
+
page: row.keys![0],
|
|
1549
|
+
clicks: row.clicks,
|
|
1550
|
+
impressions: row.impressions,
|
|
1551
|
+
ctr: row.ctr,
|
|
1552
|
+
position: row.position,
|
|
1553
|
+
}));
|
|
1554
|
+
const bounded = windowSourceRows(
|
|
1555
|
+
pages,
|
|
1556
|
+
start_row,
|
|
1557
|
+
row_limit,
|
|
1558
|
+
response.rows.length === sourceRowLimit,
|
|
1559
|
+
);
|
|
1340
1560
|
|
|
1341
|
-
|
|
1561
|
+
const payload = {
|
|
1562
|
+
pages: bounded.items,
|
|
1563
|
+
result_page: bounded.resultPage,
|
|
1564
|
+
note: SEARCH_VISIBLE_PAGES_NOTE,
|
|
1565
|
+
};
|
|
1566
|
+
return toolResponse(JSON.stringify(payload), payload);
|
|
1342
1567
|
},
|
|
1343
1568
|
);
|
|
1344
1569
|
|
|
@@ -1346,7 +1571,7 @@ class GscMcpRuntime {
|
|
|
1346
1571
|
'analytics.compare',
|
|
1347
1572
|
{
|
|
1348
1573
|
title: 'Compare Performance Between Periods',
|
|
1349
|
-
description: 'Compare Search Console performance metrics (clicks, impressions, CTR, average position) between two distinct date ranges (Period A vs Period B) for a selected dimension (query, page, country, device). Apply the same search type and optional dimension filters to both periods
|
|
1574
|
+
description: 'Compare Search Console performance metrics (clicks, impressions, CTR, average position) between two distinct date ranges (Period A vs Period B) for a selected dimension (query, page, country, device). Apply the same search type and optional dimension filters to both periods. Results are ranked by largest absolute click change, then impression change, and safely paged with limit/start_row. Percentage change is null when the baseline is zero and the comparison value differs.',
|
|
1350
1575
|
inputSchema: {
|
|
1351
1576
|
site_url: z.string().describe(SITE_URL_DESCRIPTION),
|
|
1352
1577
|
start_date_a: SEARCH_CONSOLE_DATE_SCHEMA.describe('Start date of Period A (recent, YYYY-MM-DD)'),
|
|
@@ -1388,6 +1613,8 @@ class GscMcpRuntime {
|
|
|
1388
1613
|
.describe(
|
|
1389
1614
|
"Optional Search Console filters applied identically to both periods. Use query regex filters for brand/non-brand comparisons; countries use ISO 3166-1 alpha-3 codes.",
|
|
1390
1615
|
),
|
|
1616
|
+
limit: ANALYSIS_RESULT_LIMIT_SCHEMA,
|
|
1617
|
+
start_row: RESULT_START_ROW_SCHEMA,
|
|
1391
1618
|
},
|
|
1392
1619
|
outputSchema: PERFORMANCE_COMPARISON_OUTPUT_SCHEMA,
|
|
1393
1620
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
@@ -1401,6 +1628,8 @@ class GscMcpRuntime {
|
|
|
1401
1628
|
dimension,
|
|
1402
1629
|
search_type,
|
|
1403
1630
|
dimension_filter_groups,
|
|
1631
|
+
limit = DEFAULT_ANALYSIS_RESULT_LIMIT,
|
|
1632
|
+
start_row = 0,
|
|
1404
1633
|
}) => {
|
|
1405
1634
|
assertDateRange(start_date_a, end_date_a, 'start_date_a', 'end_date_a');
|
|
1406
1635
|
assertDateRange(start_date_b, end_date_b, 'start_date_b', 'end_date_b');
|
|
@@ -1430,14 +1659,16 @@ class GscMcpRuntime {
|
|
|
1430
1659
|
});
|
|
1431
1660
|
|
|
1432
1661
|
const comparison = processPerformanceComparison(sourceA.rows, sourceB.rows);
|
|
1662
|
+
const bounded = windowKnownResults(comparison, start_row, limit);
|
|
1433
1663
|
const payload = {
|
|
1434
|
-
comparisons:
|
|
1664
|
+
comparisons: bounded.items,
|
|
1435
1665
|
pagination: {
|
|
1436
1666
|
period_a: paginationMetadata(sourceA),
|
|
1437
1667
|
period_b: paginationMetadata(sourceB),
|
|
1438
1668
|
},
|
|
1669
|
+
result_page: bounded.resultPage,
|
|
1439
1670
|
};
|
|
1440
|
-
return toolResponse(JSON.stringify(payload
|
|
1671
|
+
return toolResponse(JSON.stringify(payload), payload);
|
|
1441
1672
|
},
|
|
1442
1673
|
);
|
|
1443
1674
|
|