@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/quick-wins-schema.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { SEARCH_CONSOLE_DATE_SCHEMA } from './date-validation';
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_ANALYSIS_RESULT_LIMIT,
|
|
5
|
+
MAX_ANALYSIS_RESULT_LIMIT,
|
|
6
|
+
} from './result-bounds';
|
|
3
7
|
|
|
4
8
|
const POSITION_RANGE_ERROR = {
|
|
5
9
|
message: 'min_position must be less than or equal to max_position.',
|
|
@@ -19,6 +23,19 @@ export function createQuickWinsInputSchema(siteUrlDescription: string) {
|
|
|
19
23
|
start_date: SEARCH_CONSOLE_DATE_SCHEMA.describe('Start date (inclusive) in YYYY-MM-DD format.'),
|
|
20
24
|
end_date: SEARCH_CONSOLE_DATE_SCHEMA.describe('End date (inclusive) in YYYY-MM-DD format. Note the 2-3 day GSC data lag.'),
|
|
21
25
|
...QUICK_WINS_THRESHOLDS_SHAPE,
|
|
26
|
+
limit: z
|
|
27
|
+
.number()
|
|
28
|
+
.int()
|
|
29
|
+
.min(1)
|
|
30
|
+
.max(MAX_ANALYSIS_RESULT_LIMIT)
|
|
31
|
+
.default(DEFAULT_ANALYSIS_RESULT_LIMIT)
|
|
32
|
+
.describe('Maximum ranked quick-win results to return in this response.'),
|
|
33
|
+
start_row: z
|
|
34
|
+
.number()
|
|
35
|
+
.int()
|
|
36
|
+
.min(0)
|
|
37
|
+
.default(0)
|
|
38
|
+
.describe('Zero-based offset into the ranked quick-win result list.'),
|
|
22
39
|
})
|
|
23
40
|
.refine(
|
|
24
41
|
({ min_position, max_position }) => min_position <= max_position,
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export const STRUCTURED_RESULT_BYTE_BUDGET = 48_000;
|
|
2
|
+
export const DEFAULT_ANALYSIS_RESULT_LIMIT = 50;
|
|
3
|
+
export const MAX_ANALYSIS_RESULT_LIMIT = 100;
|
|
4
|
+
export const MAX_DIRECT_SOURCE_ROWS = 500;
|
|
5
|
+
|
|
6
|
+
export interface BoundedItems<T> {
|
|
7
|
+
items: T[];
|
|
8
|
+
byteLimitReached: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function jsonByteLength(value: unknown): number {
|
|
12
|
+
return new TextEncoder().encode(JSON.stringify(value)).byteLength;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Keep one structured-content list inside a conservative byte budget.
|
|
17
|
+
*
|
|
18
|
+
* Count limits alone are not sufficient because Search Console keys are
|
|
19
|
+
* caller-controlled strings (queries and URLs). This helper therefore applies
|
|
20
|
+
* both the requested item ceiling and an encoded-JSON byte ceiling.
|
|
21
|
+
*/
|
|
22
|
+
export function takeBoundedItems<T>(
|
|
23
|
+
items: readonly T[],
|
|
24
|
+
maxItems: number,
|
|
25
|
+
byteBudget = STRUCTURED_RESULT_BYTE_BUDGET,
|
|
26
|
+
): BoundedItems<T> {
|
|
27
|
+
if (!Number.isInteger(maxItems) || maxItems < 1) {
|
|
28
|
+
throw new Error('maxItems must be a positive integer.');
|
|
29
|
+
}
|
|
30
|
+
if (!Number.isInteger(byteBudget) || byteBudget < 1) {
|
|
31
|
+
throw new Error('byteBudget must be a positive integer.');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const bounded: T[] = [];
|
|
35
|
+
// Account for the surrounding JSON array. Per-item commas are added below.
|
|
36
|
+
let usedBytes = 2;
|
|
37
|
+
let byteLimitReached = false;
|
|
38
|
+
|
|
39
|
+
for (const item of items.slice(0, maxItems)) {
|
|
40
|
+
const itemBytes = jsonByteLength(item) + (bounded.length > 0 ? 1 : 0);
|
|
41
|
+
if (usedBytes + itemBytes > byteBudget) {
|
|
42
|
+
byteLimitReached = true;
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
bounded.push(item);
|
|
46
|
+
usedBytes += itemBytes;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { items: bounded, byteLimitReached };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ResultPageMetadata {
|
|
53
|
+
start_row: number;
|
|
54
|
+
limit: number;
|
|
55
|
+
returned_count: number;
|
|
56
|
+
total_count?: number;
|
|
57
|
+
has_more: boolean;
|
|
58
|
+
truncated: boolean;
|
|
59
|
+
byte_limit_reached: boolean;
|
|
60
|
+
next_start_row?: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function resultPageMetadata(input: {
|
|
64
|
+
startRow: number;
|
|
65
|
+
limit: number;
|
|
66
|
+
returnedCount: number;
|
|
67
|
+
totalCount?: number;
|
|
68
|
+
hasMore: boolean;
|
|
69
|
+
byteLimitReached: boolean;
|
|
70
|
+
}): ResultPageMetadata {
|
|
71
|
+
const {
|
|
72
|
+
startRow,
|
|
73
|
+
limit,
|
|
74
|
+
returnedCount,
|
|
75
|
+
totalCount,
|
|
76
|
+
hasMore,
|
|
77
|
+
byteLimitReached,
|
|
78
|
+
} = input;
|
|
79
|
+
return {
|
|
80
|
+
start_row: startRow,
|
|
81
|
+
limit,
|
|
82
|
+
returned_count: returnedCount,
|
|
83
|
+
...(totalCount !== undefined ? { total_count: totalCount } : {}),
|
|
84
|
+
has_more: hasMore,
|
|
85
|
+
truncated: hasMore || byteLimitReached,
|
|
86
|
+
byte_limit_reached: byteLimitReached,
|
|
87
|
+
...(hasMore && returnedCount > 0
|
|
88
|
+
? { next_start_row: startRow + returnedCount }
|
|
89
|
+
: {}),
|
|
90
|
+
};
|
|
91
|
+
}
|