@gscdump/cli 1.4.0 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/{_chunks/analysis-local.mjs → analysis-local.mjs} +4 -3
  2. package/dist/{_chunks/auth.mjs → auth.mjs} +2 -2
  3. package/dist/cli.d.mts +1 -19
  4. package/dist/cli.mjs +25 -25
  5. package/dist/{_chunks → commands}/analyze.mjs +3 -3
  6. package/dist/{_chunks/auth2.mjs → commands/auth.mjs} +4 -4
  7. package/dist/commands/compact.mjs +128 -0
  8. package/dist/{_chunks/config2.mjs → commands/config.mjs} +4 -4
  9. package/dist/{_chunks → commands}/doctor.mjs +8 -7
  10. package/dist/{_chunks → commands}/dump.mjs +5 -4
  11. package/dist/{_chunks → commands}/entities.mjs +2 -2
  12. package/dist/commands/export.mjs +76 -0
  13. package/dist/commands/gc.mjs +74 -0
  14. package/dist/{_chunks → commands}/indexing.mjs +3 -3
  15. package/dist/{_chunks → commands}/init.mjs +5 -5
  16. package/dist/{_chunks → commands}/inspect.mjs +3 -3
  17. package/dist/commands/mcp.mjs +57 -0
  18. package/dist/{_chunks → commands}/profile.mjs +7 -17
  19. package/dist/{_chunks → commands}/query.mjs +5 -4
  20. package/dist/{_chunks → commands}/report.mjs +2 -2
  21. package/dist/commands/rollups.mjs +134 -0
  22. package/dist/{_chunks → commands}/sitemaps.mjs +3 -3
  23. package/dist/{_chunks → commands}/sites.mjs +3 -3
  24. package/dist/commands/stats.mjs +118 -0
  25. package/dist/commands/store-purge.mjs +93 -0
  26. package/dist/commands/store.mjs +40 -0
  27. package/dist/{_chunks → commands}/sync.mjs +3 -2
  28. package/dist/config.mjs +43 -0
  29. package/dist/{_chunks/context.mjs → context.mjs} +3 -9
  30. package/dist/{_chunks/env-file.mjs → env-file.mjs} +3 -3
  31. package/dist/{_chunks/environment.mjs → environment.mjs} +1 -1
  32. package/dist/index.d.mts +2 -0
  33. package/dist/index.mjs +2 -0
  34. package/dist/local-store.mjs +7 -0
  35. package/dist/mcp/errors.mjs +85 -0
  36. package/dist/mcp/handlers/analytics.mjs +1 -0
  37. package/dist/mcp/handlers/diagnostics.mjs +134 -0
  38. package/dist/mcp/handlers/index.mjs +7 -0
  39. package/dist/mcp/handlers/indexing.mjs +27 -0
  40. package/dist/mcp/handlers/query.mjs +5 -0
  41. package/dist/mcp/handlers/reports.mjs +72 -0
  42. package/dist/mcp/handlers/sites.mjs +8 -0
  43. package/dist/mcp/server/index.mjs +417 -0
  44. package/dist/mcp/types.mjs +86 -0
  45. package/dist/package.mjs +2 -0
  46. package/dist/runtime.d.mts +20 -0
  47. package/dist/runtime.mjs +38 -0
  48. package/dist/{_chunks/utils.mjs → utils.mjs} +4 -4
  49. package/package.json +8 -8
  50. package/dist/_chunks/config.mjs +0 -93
  51. package/dist/_chunks/mcp.mjs +0 -867
  52. package/dist/_chunks/store.mjs +0 -628
  53. /package/dist/{_chunks/error-handler.mjs → error-handler.mjs} +0 -0
  54. /package/dist/{_chunks/native-duckdb.mjs → native-duckdb.mjs} +0 -0
@@ -1,867 +0,0 @@
1
- import { loadConfig } from "./config.mjs";
2
- import { VERSION } from "./utils.mjs";
3
- import { loadTokens, resolveAuth, resolveBYOK, resolveServiceAccount } from "./auth.mjs";
4
- import process from "node:process";
5
- import { defineCommand } from "citty";
6
- import { isQueryError } from "gscdump/query";
7
- import { defaultAnalyzerRegistry } from "@gscdump/analysis/registry";
8
- import { createGscApiQuerySource } from "@gscdump/engine-gsc-api";
9
- import { err, ok, unwrapResult } from "gscdump/result";
10
- import { addSite, batchInspectUrls, batchRequestIndexing, deleteSite, discoverSitemap, fetchSitemap, fetchSitesWithSitemaps, getIndexingMetadata, getVerificationToken, getVerifiedSite, googleSearchConsole, hasGscWriteScope, hasIndexingScope, listVerifiedSites, requestIndexing, runSequentialBatch, unverifySite, verifySite } from "gscdump";
11
- import { ofetch } from "ofetch";
12
- import { isAnalysisError } from "@gscdump/analysis/errors";
13
- import { isEngineError } from "@gscdump/engine/errors";
14
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
15
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
16
- import { z } from "zod";
17
- import { defaultReportRegistry, runReport } from "@gscdump/analysis/report";
18
- import { resolveWindow } from "@gscdump/engine/period";
19
- const REQUIRED_SCOPES = [
20
- "https://www.googleapis.com/auth/webmasters",
21
- "https://www.googleapis.com/auth/indexing",
22
- "https://www.googleapis.com/auth/siteverification"
23
- ];
24
- const FETCH_TIMEOUT_MS = 5e3;
25
- const TIME_SKEW_WARN_MS = 5 * 6e4;
26
- function hasGoogleScope(scopes, scope) {
27
- const suffix = scope.replace("https://www.googleapis.com/auth/", "");
28
- return scopes.includes(scope) || scopes.includes(suffix);
29
- }
30
- function missingRequiredScopes(scopes) {
31
- return REQUIRED_SCOPES.filter((scope) => {
32
- if (scope.endsWith("/webmasters")) return !hasGscWriteScope(scopes);
33
- if (scope.endsWith("/indexing")) return !hasIndexingScope(scopes);
34
- return !hasGoogleScope(scopes, scope);
35
- });
36
- }
37
- async function diagnostics(_input, ctx) {
38
- const checks = [];
39
- const token = await resolveAccessToken(ctx.auth);
40
- if (!token) {
41
- checks.push({
42
- name: "auth",
43
- status: "fail",
44
- detail: "no usable access token (refresh failed or no credentials)"
45
- });
46
- return {
47
- ok: false,
48
- checks
49
- };
50
- }
51
- const [tokenInfo, timeCheck, gscReachable, indexingReachable, sites] = await Promise.all([
52
- ofetch("https://oauth2.googleapis.com/tokeninfo", { query: { access_token: token } }).catch((e) => ({ error: e.message })),
53
- probeTimeSkew(),
54
- probeReachable("https://searchconsole.googleapis.com/$discovery/rest?version=v1"),
55
- probeReachable("https://indexing.googleapis.com/$discovery/rest?version=v3"),
56
- ctx.client.sites().catch((e) => e)
57
- ]);
58
- if ("error" in tokenInfo) checks.push({
59
- name: "auth",
60
- status: "fail",
61
- detail: `tokeninfo failed: ${tokenInfo.error}`
62
- });
63
- else {
64
- checks.push({
65
- name: "auth",
66
- status: "pass",
67
- detail: tokenInfo.email ?? "token valid"
68
- });
69
- const scopes = tokenInfo.scope ? tokenInfo.scope.split(/\s+/) : [];
70
- const missing = missingRequiredScopes(scopes);
71
- checks.push(missing.length > 0 ? {
72
- name: "auth.scopes",
73
- status: "warn",
74
- detail: `missing: ${missing.join(", ")}`
75
- } : {
76
- name: "auth.scopes",
77
- status: "pass",
78
- detail: `${scopes.length} granted`
79
- });
80
- }
81
- checks.push(timeCheck);
82
- checks.push({
83
- name: "gsc.api",
84
- status: gscReachable ? "pass" : "warn",
85
- detail: gscReachable ? "reachable" : "searchconsole.googleapis.com unreachable"
86
- });
87
- checks.push({
88
- name: "indexing.api",
89
- status: indexingReachable ? "pass" : "warn",
90
- detail: indexingReachable ? "reachable" : "indexing.googleapis.com unreachable"
91
- });
92
- if (sites instanceof Error) checks.push({
93
- name: "gsc.sites",
94
- status: "fail",
95
- detail: `sites() failed: ${sites.message}`
96
- });
97
- else {
98
- const verified = sites.filter((s) => s.permissionLevel !== "siteUnverifiedUser").length;
99
- checks.push({
100
- name: "gsc.sites",
101
- status: "pass",
102
- detail: `${sites.length} site(s) accessible (${verified} verified)`
103
- });
104
- }
105
- return {
106
- ok: !checks.some((c) => c.status === "fail"),
107
- checks
108
- };
109
- }
110
- async function resolveAccessToken(auth) {
111
- if (typeof auth === "string") return auth;
112
- if (auth && typeof auth === "object" && "getAccessToken" in auth) return (await auth.getAccessToken().catch(() => null))?.token ?? null;
113
- return null;
114
- }
115
- async function probeTimeSkew() {
116
- const dateHeader = await ofetch.raw("https://oauth2.googleapis.com/tokeninfo", {
117
- method: "GET",
118
- timeout: FETCH_TIMEOUT_MS
119
- }).then((r) => r.headers.get("date")).catch((e) => e?.response?.headers?.get("date") ?? null);
120
- if (!dateHeader) return {
121
- name: "time",
122
- status: "warn",
123
- detail: "no Date header"
124
- };
125
- const remoteMs = Date.parse(dateHeader);
126
- if (!Number.isFinite(remoteMs)) return {
127
- name: "time",
128
- status: "warn",
129
- detail: `unparseable Date header: ${dateHeader}`
130
- };
131
- const skewMs = Date.now() - remoteMs;
132
- const human = `${skewMs >= 0 ? "+" : ""}${(skewMs / 1e3).toFixed(1)}s`;
133
- if (Math.abs(skewMs) > TIME_SKEW_WARN_MS) return {
134
- name: "time",
135
- status: "warn",
136
- detail: `local clock ${human} off Google`
137
- };
138
- return {
139
- name: "time",
140
- status: "pass",
141
- detail: `in sync (${human})`
142
- };
143
- }
144
- async function probeReachable(url) {
145
- return ofetch.raw(url, {
146
- method: "GET",
147
- timeout: FETCH_TIMEOUT_MS
148
- }).then(() => true).catch(() => false);
149
- }
150
- async function requestIndexing$1(input, ctx) {
151
- return requestIndexing(ctx.client, input.url, { type: input.type || "URL_UPDATED" });
152
- }
153
- async function getIndexingStatus(input, ctx) {
154
- return getIndexingMetadata(ctx.client, input.url);
155
- }
156
- async function batchRequestIndexing$1(input, ctx) {
157
- const results = await batchRequestIndexing(ctx.client, input.urls, {
158
- type: input.type || "URL_UPDATED",
159
- delayMs: input.delayMs || 100
160
- });
161
- return {
162
- results,
163
- success: results.length,
164
- failed: 0
165
- };
166
- }
167
- async function batchInspectUrls$1(input, ctx) {
168
- const results = await batchInspectUrls(ctx.client, input.siteUrl, input.urls, { delayMs: input.delayMs || 200 });
169
- return {
170
- results,
171
- indexed: results.filter((r) => r.isIndexed).length,
172
- notIndexed: results.filter((r) => !r.isIndexed).length
173
- };
174
- }
175
- const PERIODS = [
176
- "7d",
177
- "28d",
178
- "30d",
179
- "90d",
180
- "180d",
181
- "365d",
182
- "mtd",
183
- "ytd",
184
- "custom"
185
- ];
186
- const COMPARISONS = [
187
- "none",
188
- "prev-period",
189
- "yoy"
190
- ];
191
- const mcpHandlerErrors = {
192
- unknownReport(id, available) {
193
- return {
194
- kind: "unknown-report",
195
- id,
196
- available,
197
- message: `Unknown report id "${id}". Available: ${available.join(", ")}`
198
- };
199
- },
200
- unknownPeriod(value) {
201
- return {
202
- kind: "unknown-period",
203
- value,
204
- supported: PERIODS,
205
- message: `Unknown period "${value}". Supported: ${PERIODS.join(", ")}.`
206
- };
207
- },
208
- unknownComparison(value) {
209
- return {
210
- kind: "unknown-comparison",
211
- value,
212
- supported: COMPARISONS,
213
- message: `Unknown comparison "${value}". Supported: ${COMPARISONS.join(", ")}.`
214
- };
215
- },
216
- noValidDimension() {
217
- return {
218
- kind: "no-valid-dimension",
219
- message: "At least one valid dimension required"
220
- };
221
- }
222
- };
223
- function mcpHandlerErrorToException(error) {
224
- const exception = new Error(error.message);
225
- exception.mcpHandlerError = error;
226
- return exception;
227
- }
228
- function enrichToolError(error) {
229
- const query = asQueryError(error);
230
- if (query) return /* @__PURE__ */ new Error(`[query:${query.kind}] ${query.message}`);
231
- const analysis = asAnalysisError(error);
232
- if (analysis) {
233
- const cause = analysis.kind === "required-step-failed" ? asEngineError(analysis.cause) : null;
234
- const tail = cause ? ` (engine:${cause.kind})` : "";
235
- return /* @__PURE__ */ new Error(`[analysis:${analysis.kind}]${tail} ${analysis.message}`);
236
- }
237
- const engine = asEngineError(error);
238
- if (engine) return /* @__PURE__ */ new Error(`[engine:${engine.kind}] ${engine.message}`);
239
- return null;
240
- }
241
- function asQueryError(error) {
242
- if (isQueryError(error)) return error;
243
- const tagged = error?.queryError;
244
- return isQueryError(tagged) ? tagged : null;
245
- }
246
- function asEngineError(error) {
247
- if (isEngineError(error)) return error;
248
- const tagged = error?.engineError;
249
- return isEngineError(tagged) ? tagged : null;
250
- }
251
- function asAnalysisError(error) {
252
- if (isAnalysisError(error)) return error;
253
- const tagged = error?.analysisError;
254
- return isAnalysisError(tagged) ? tagged : null;
255
- }
256
- const PERIOD_ALIASES = {
257
- "7d": "last-7d",
258
- "28d": "last-28d",
259
- "30d": "last-30d",
260
- "90d": "last-90d",
261
- "180d": "last-180d",
262
- "365d": "last-365d",
263
- "mtd": "mtd",
264
- "ytd": "ytd",
265
- "custom": "custom"
266
- };
267
- const COMPARISON_ALIASES = {
268
- "none": "none",
269
- "prev": "prev-period",
270
- "prev-period": "prev-period",
271
- "prior": "prev-period",
272
- "yoy": "yoy"
273
- };
274
- function listReports() {
275
- return defaultReportRegistry.listReports().map((r) => ({
276
- id: r.id,
277
- description: r.description,
278
- defaultPeriod: r.defaultPeriod,
279
- defaultComparison: r.defaultComparison,
280
- argsSpec: r.argsSpec
281
- }));
282
- }
283
- async function runReportHandlerResult(input, ctx) {
284
- const report = defaultReportRegistry.getReport(input.id);
285
- if (!report) return err(mcpHandlerErrors.unknownReport(input.id, defaultReportRegistry.listReportIds()));
286
- const preset = input.period ? PERIOD_ALIASES[input.period.toLowerCase()] ?? null : report.defaultPeriod;
287
- if (!preset) return err(mcpHandlerErrors.unknownPeriod(input.period ?? ""));
288
- const comparison = input.comparison ? COMPARISON_ALIASES[input.comparison.toLowerCase()] ?? null : report.defaultComparison;
289
- if (!comparison) return err(mcpHandlerErrors.unknownComparison(input.comparison ?? ""));
290
- const window = resolveWindow({
291
- preset,
292
- comparison,
293
- start: input.start,
294
- end: input.end
295
- });
296
- if (input.prevStart && input.prevEnd) window.comparison = {
297
- start: input.prevStart,
298
- end: input.prevEnd
299
- };
300
- const params = {};
301
- if (input.maxFindings != null) params.maxFindings = input.maxFindings;
302
- return ok(await runReport(report, {
303
- source: createGscApiQuerySource({
304
- client: ctx.client,
305
- siteUrl: input.siteUrl
306
- }),
307
- analyzers: defaultAnalyzerRegistry,
308
- ctx: {
309
- site: input.siteUrl,
310
- window,
311
- params,
312
- registryVersion: defaultReportRegistry.version
313
- }
314
- }));
315
- }
316
- async function runReportHandler(input, ctx) {
317
- return unwrapResult(await runReportHandlerResult(input, ctx).catch((thrown) => {
318
- throw enrichToolError(thrown) ?? thrown;
319
- }), mcpHandlerErrorToException);
320
- }
321
- async function listSitesWithSitemaps(_input, ctx) {
322
- return fetchSitesWithSitemaps(ctx.client);
323
- }
324
- async function getSitemap(input, ctx) {
325
- return fetchSitemap(ctx.client, input.siteUrl, input.feedpath);
326
- }
327
- const periodSchema = z.object({
328
- start: z.string().describe("Start date (YYYY-MM-DD)"),
329
- end: z.string().describe("End date (YYYY-MM-DD)")
330
- }).describe("Date range for the query");
331
- const siteUrlSchema = z.string().describe("GSC property URL (e.g., sc-domain:example.com or https://example.com/)");
332
- const queryOptionsSchema = z.object({
333
- type: z.enum([
334
- "web",
335
- "image",
336
- "video",
337
- "news",
338
- "discover",
339
- "googleNews"
340
- ]).optional().describe("Data type"),
341
- dataState: z.enum(["final", "all"]).optional().describe("Data state: final (settled) or all (includes fresh)"),
342
- aggregationType: z.enum(["byPage", "byProperty"]).optional().describe("Aggregation: byPage or byProperty")
343
- }).optional();
344
- const listSitesInput = z.object({});
345
- const listSitemapsInput = z.object({ siteUrl: siteUrlSchema });
346
- z.object({
347
- siteUrl: siteUrlSchema,
348
- period: periodSchema,
349
- comparePrevious: z.boolean().optional().describe("Include previous period comparison"),
350
- options: queryOptionsSchema
351
- });
352
- z.object({
353
- siteUrl: siteUrlSchema,
354
- period: periodSchema,
355
- url: z.string().describe("Page URL to fetch details for")
356
- });
357
- z.object({
358
- siteUrl: siteUrlSchema,
359
- period: periodSchema,
360
- keyword: z.string().describe("Keyword to fetch details for")
361
- });
362
- const inspectUrlInput = z.object({
363
- siteUrl: siteUrlSchema,
364
- inspectionUrl: z.string().describe("URL to inspect")
365
- });
366
- const requestIndexingInput = z.object({
367
- url: z.string().describe("URL to request indexing for"),
368
- type: z.enum(["URL_UPDATED", "URL_DELETED"]).optional().describe("Notification type")
369
- });
370
- const getIndexingStatusInput = z.object({ url: z.string().describe("URL to get indexing status for") });
371
- z.object({
372
- siteUrl: siteUrlSchema,
373
- period: periodSchema,
374
- dimensions: z.array(z.enum([
375
- "date",
376
- "query",
377
- "page",
378
- "country",
379
- "device",
380
- "searchAppearance"
381
- ])).describe("Dimensions to group by"),
382
- rowLimit: z.number().optional().describe("Max rows (default 25000)"),
383
- options: queryOptionsSchema
384
- });
385
- const sitemapInput = z.object({
386
- siteUrl: siteUrlSchema,
387
- feedpath: z.string().describe("Sitemap URL (e.g., https://example.com/sitemap.xml)")
388
- });
389
- const batchRequestIndexingInput = z.object({
390
- urls: z.array(z.string()).describe("URLs to request indexing for"),
391
- type: z.enum(["URL_UPDATED", "URL_DELETED"]).optional().describe("Notification type"),
392
- delayMs: z.number().optional().describe("Delay between requests in ms (default 100)")
393
- });
394
- const batchInspectUrlsInput = z.object({
395
- siteUrl: siteUrlSchema,
396
- urls: z.array(z.string()).describe("URLs to inspect"),
397
- delayMs: z.number().optional().describe("Delay between requests in ms (default 200)")
398
- });
399
- const listReportsInput = z.object({});
400
- const runReportInput = z.object({
401
- siteUrl: siteUrlSchema,
402
- id: z.string().describe("Report id (e.g. health, movers, opportunities, risks). See list-reports."),
403
- period: z.string().optional().describe("Window: 7d|28d|30d|90d|180d|365d|mtd|ytd|custom (default per report)."),
404
- comparison: z.string().optional().describe("Comparison: none|prev-period|yoy (default per report)."),
405
- start: z.string().optional().describe("Custom window start (YYYY-MM-DD); requires period=custom."),
406
- end: z.string().optional().describe("Custom window end (YYYY-MM-DD); requires period=custom."),
407
- prevStart: z.string().optional().describe("Override comparison-window start."),
408
- prevEnd: z.string().optional().describe("Override comparison-window end."),
409
- maxFindings: z.number().optional().describe("Cap findings per section (per-report default ~5).")
410
- });
411
- async function runMcpSearchAnalyticsQuery(client, args) {
412
- const totalLimit = Math.max(0, args.rowLimit ?? 25e3);
413
- const pageSize = Math.min(totalLimit, 25e3);
414
- const allRows = [];
415
- let startRow = 0;
416
- while (allRows.length < totalLimit) {
417
- const remaining = totalLimit - allRows.length;
418
- const currentLimit = Math.min(pageSize, remaining);
419
- if (currentLimit <= 0) break;
420
- const rows = ((await client.searchAnalytics.query(args.siteUrl, {
421
- startDate: args.startDate,
422
- endDate: args.endDate,
423
- dimensions: args.dimensions,
424
- rowLimit: currentLimit,
425
- startRow,
426
- ...args.type ? { type: args.type } : {},
427
- ...args.dataState ? { dataState: args.dataState } : {},
428
- ...args.aggregationType ? { aggregationType: args.aggregationType } : {},
429
- ...args.dimensionFilterGroups ? { dimensionFilterGroups: args.dimensionFilterGroups.map((g) => ({
430
- groupType: g.groupType ?? "and",
431
- filters: g.filters
432
- })) } : {}
433
- })).rows || []).map((row) => {
434
- const result = {
435
- clicks: row.clicks ?? 0,
436
- impressions: row.impressions ?? 0,
437
- ctr: row.ctr ?? 0,
438
- position: row.position ?? 0
439
- };
440
- args.dimensions.forEach((dim, i) => {
441
- result[dim] = row.keys?.[i];
442
- });
443
- return result;
444
- });
445
- if (rows.length === 0) break;
446
- allRows.push(...rows);
447
- startRow += rows.length;
448
- }
449
- return {
450
- siteUrl: args.siteUrl,
451
- rowCount: allRows.length,
452
- rows: allRows
453
- };
454
- }
455
- function createGscMcpServer(options) {
456
- const { name = "gscdump", version = "1.0.0", getAuth } = options;
457
- const server = new McpServer({
458
- name,
459
- version
460
- });
461
- const auth = async () => Promise.resolve(getAuth());
462
- const getContext = async () => {
463
- const a = await auth();
464
- return {
465
- auth: a,
466
- client: googleSearchConsole(a)
467
- };
468
- };
469
- const getClient = async () => googleSearchConsole(await auth());
470
- server.registerTool("list-sites", {
471
- description: "List all Google Search Console sites visible to the authenticated user.",
472
- inputSchema: listSitesInput.shape
473
- }, async () => {
474
- const sites = (await (await getClient()).sites()).filter((s) => s.siteUrl && s.permissionLevel !== "siteUnverifiedUser").map((s) => ({
475
- siteUrl: s.siteUrl,
476
- permissionLevel: s.permissionLevel || "unknown"
477
- }));
478
- return { content: [{
479
- type: "text",
480
- text: JSON.stringify(sites, null, 2)
481
- }] };
482
- });
483
- server.registerTool("list-sites-with-sitemaps", {
484
- description: "List all GSC sites with their sitemaps",
485
- inputSchema: listSitesInput.shape
486
- }, async (args) => {
487
- const result = await listSitesWithSitemaps(args, await getContext());
488
- return { content: [{
489
- type: "text",
490
- text: JSON.stringify(result, null, 2)
491
- }] };
492
- });
493
- server.registerTool("list-sitemaps", {
494
- description: "List sitemaps for a specific site",
495
- inputSchema: listSitemapsInput.shape
496
- }, async (args) => {
497
- const sitemaps = await (await getClient()).sitemaps.list(args.siteUrl);
498
- return { content: [{
499
- type: "text",
500
- text: JSON.stringify(sitemaps, null, 2)
501
- }] };
502
- });
503
- server.registerTool("get-sitemap", {
504
- description: "Get details for a specific sitemap",
505
- inputSchema: sitemapInput.shape
506
- }, async (args) => {
507
- const result = await getSitemap(args, await getContext());
508
- return { content: [{
509
- type: "text",
510
- text: JSON.stringify(result, null, 2)
511
- }] };
512
- });
513
- server.registerTool("submit-sitemap", {
514
- description: "Submit a sitemap to Google Search Console",
515
- inputSchema: sitemapInput.shape
516
- }, async (args) => {
517
- await (await getClient()).sitemaps.submit(args.siteUrl, args.feedpath);
518
- return { content: [{
519
- type: "text",
520
- text: JSON.stringify({ success: true }, null, 2)
521
- }] };
522
- });
523
- server.registerTool("delete-sitemap", {
524
- description: "Delete a sitemap from Google Search Console",
525
- inputSchema: sitemapInput.shape
526
- }, async (args) => {
527
- await (await getClient()).sitemaps.delete(args.siteUrl, args.feedpath);
528
- return { content: [{
529
- type: "text",
530
- text: JSON.stringify({ success: true }, null, 2)
531
- }] };
532
- });
533
- server.registerTool("list-reports", {
534
- description: "List available reports (intent-keyed analyzer compositions). Returns id, description, default period/comparison, and per-report argsSpec.",
535
- inputSchema: listReportsInput.shape
536
- }, async () => {
537
- const result = listReports();
538
- return { content: [{
539
- type: "text",
540
- text: JSON.stringify(result, null, 2)
541
- }] };
542
- });
543
- server.registerTool("run-report", {
544
- description: "Run a report against the GSC API. Returns a structured ReportResult with bounded findings per section. See list-reports for ids.",
545
- inputSchema: runReportInput.shape
546
- }, async (args) => {
547
- const result = await runReportHandler(args, await getContext());
548
- return { content: [{
549
- type: "text",
550
- text: JSON.stringify(result, null, 2)
551
- }] };
552
- });
553
- const filterOperatorSchema = z.enum([
554
- "equals",
555
- "notEquals",
556
- "contains",
557
- "notContains",
558
- "includingRegex",
559
- "excludingRegex"
560
- ]);
561
- const dimensionFilterSchema = z.object({
562
- dimension: z.enum([
563
- "date",
564
- "query",
565
- "page",
566
- "country",
567
- "device",
568
- "searchAppearance"
569
- ]),
570
- operator: filterOperatorSchema,
571
- expression: z.string()
572
- });
573
- const filterGroupSchema = z.object({
574
- groupType: z.enum(["and"]).optional().describe("Always \"and\"; multiple groups are OR-ed together"),
575
- filters: z.array(dimensionFilterSchema)
576
- });
577
- server.registerTool("query", {
578
- description: "Run a custom search analytics query. Supports dimension filters (regex/contains/equals) via dimensionFilterGroups; multiple groups are OR-ed.",
579
- inputSchema: z.object({
580
- siteUrl: z.string().describe("GSC property URL (e.g., sc-domain:example.com)"),
581
- startDate: z.string().describe("Start date (YYYY-MM-DD)"),
582
- endDate: z.string().describe("End date (YYYY-MM-DD)"),
583
- dimensions: z.array(z.enum([
584
- "date",
585
- "query",
586
- "page",
587
- "country",
588
- "device",
589
- "searchAppearance"
590
- ])).describe("Dimensions to group by"),
591
- rowLimit: z.number().optional().describe("Max rows (default 25000)"),
592
- type: z.enum([
593
- "web",
594
- "image",
595
- "video",
596
- "news",
597
- "discover",
598
- "googleNews"
599
- ]).optional().describe("Search type"),
600
- dataState: z.enum(["final", "all"]).optional().describe("Data state: final (settled) or all (includes fresh)"),
601
- aggregationType: z.enum(["byPage", "byProperty"]).optional().describe("Aggregation type"),
602
- dimensionFilterGroups: z.array(filterGroupSchema).optional().describe("Filter groups (each \"and\"-ed internally; multiple groups are OR-ed)")
603
- }).shape
604
- }, async ({ siteUrl, startDate, endDate, dimensions, rowLimit, type, dataState, aggregationType, dimensionFilterGroups }) => {
605
- const result = await runMcpSearchAnalyticsQuery(await getClient(), {
606
- siteUrl,
607
- startDate,
608
- endDate,
609
- dimensions,
610
- rowLimit,
611
- type,
612
- dataState,
613
- aggregationType,
614
- dimensionFilterGroups
615
- });
616
- return { content: [{
617
- type: "text",
618
- text: JSON.stringify(result, null, 2)
619
- }] };
620
- });
621
- server.registerTool("inspect-url", {
622
- description: "Inspect a URL to check its indexing status in Google Search Console",
623
- inputSchema: inspectUrlInput.shape
624
- }, async (args) => {
625
- const result = await (await getClient()).inspect(args.siteUrl, args.inspectionUrl);
626
- return { content: [{
627
- type: "text",
628
- text: JSON.stringify(result, null, 2)
629
- }] };
630
- });
631
- server.registerTool("request-indexing", {
632
- description: "Request Google to index or remove a URL via the Indexing API",
633
- inputSchema: requestIndexingInput.shape
634
- }, async (args) => {
635
- const result = await requestIndexing$1(args, await getContext());
636
- return { content: [{
637
- type: "text",
638
- text: JSON.stringify(result, null, 2)
639
- }] };
640
- });
641
- server.registerTool("get-indexing-status", {
642
- description: "Get indexing status metadata for a URL",
643
- inputSchema: getIndexingStatusInput.shape
644
- }, async (args) => {
645
- const result = await getIndexingStatus(args, await getContext());
646
- return { content: [{
647
- type: "text",
648
- text: JSON.stringify(result, null, 2)
649
- }] };
650
- });
651
- server.registerTool("batch-request-indexing", {
652
- description: "Batch request indexing for multiple URLs with rate limiting",
653
- inputSchema: batchRequestIndexingInput.shape
654
- }, async (args) => {
655
- const result = await batchRequestIndexing$1(args, await getContext());
656
- return { content: [{
657
- type: "text",
658
- text: JSON.stringify(result, null, 2)
659
- }] };
660
- });
661
- server.registerTool("batch-inspect-urls", {
662
- description: "Batch inspect multiple URLs to check their indexing status",
663
- inputSchema: batchInspectUrlsInput.shape
664
- }, async (args) => {
665
- const result = await batchInspectUrls$1(args, await getContext());
666
- return { content: [{
667
- type: "text",
668
- text: JSON.stringify(result, null, 2)
669
- }] };
670
- });
671
- server.registerTool("diagnostics", {
672
- description: "Run health checks on the active GSC connection: auth/scopes, time skew, API reachability, sites count.",
673
- inputSchema: listSitesInput.shape
674
- }, async (args) => {
675
- const result = await diagnostics(args, await getContext());
676
- return { content: [{
677
- type: "text",
678
- text: JSON.stringify(result, null, 2)
679
- }] };
680
- });
681
- server.registerTool("add-site", {
682
- description: "Register a property in Search Console (unverified). Verify ownership separately.",
683
- inputSchema: z.object({ siteUrl: z.string().describe("Property URL (https://example.com/ or sc-domain:example.com)") }).shape
684
- }, async ({ siteUrl }) => {
685
- await addSite(await getClient(), siteUrl);
686
- return { content: [{
687
- type: "text",
688
- text: JSON.stringify({
689
- siteUrl,
690
- status: "added",
691
- verified: false
692
- }, null, 2)
693
- }] };
694
- });
695
- server.registerTool("delete-site", {
696
- description: "Remove a property from Search Console.",
697
- inputSchema: z.object({ siteUrl: z.string().describe("Property URL") }).shape
698
- }, async ({ siteUrl }) => {
699
- await deleteSite(await getClient(), siteUrl);
700
- return { content: [{
701
- type: "text",
702
- text: JSON.stringify({
703
- siteUrl,
704
- status: "deleted"
705
- }, null, 2)
706
- }] };
707
- });
708
- const verificationMethodSchema = z.enum([
709
- "META",
710
- "FILE",
711
- "DNS_TXT",
712
- "DNS_CNAME",
713
- "ANALYTICS",
714
- "TAG_MANAGER"
715
- ]);
716
- server.registerTool("get-verification-token", {
717
- description: "Get a verification token to place on the site or in DNS.",
718
- inputSchema: z.object({
719
- siteUrl: z.string(),
720
- method: verificationMethodSchema
721
- }).shape
722
- }, async ({ siteUrl, method }) => {
723
- const result = await getVerificationToken(await getClient(), siteUrl, method);
724
- return { content: [{
725
- type: "text",
726
- text: JSON.stringify({
727
- siteUrl,
728
- method,
729
- token: result.token,
730
- site: result.site
731
- }, null, 2)
732
- }] };
733
- });
734
- server.registerTool("verify-site", {
735
- description: "Trigger Google to validate a placed verification token.",
736
- inputSchema: z.object({
737
- siteUrl: z.string(),
738
- method: verificationMethodSchema
739
- }).shape
740
- }, async ({ siteUrl, method }) => {
741
- const resource = await verifySite(await getClient(), siteUrl, method);
742
- return { content: [{
743
- type: "text",
744
- text: JSON.stringify({
745
- siteUrl,
746
- method,
747
- resource
748
- }, null, 2)
749
- }] };
750
- });
751
- server.registerTool("list-verified-sites", {
752
- description: "List verified WebResources from the Site Verification API.",
753
- inputSchema: z.object({}).shape
754
- }, async () => {
755
- const resources = await listVerifiedSites(await getClient());
756
- return { content: [{
757
- type: "text",
758
- text: JSON.stringify(resources, null, 2)
759
- }] };
760
- });
761
- server.registerTool("get-verified-site", {
762
- description: "Fetch a single verified WebResource by id.",
763
- inputSchema: z.object({ id: z.string().describe("WebResource id (from list-verified-sites)") }).shape
764
- }, async ({ id }) => {
765
- const resource = await getVerifiedSite(await getClient(), id);
766
- return { content: [{
767
- type: "text",
768
- text: JSON.stringify(resource, null, 2)
769
- }] };
770
- });
771
- server.registerTool("unverify-site", {
772
- description: "Drop the calling user's verified ownership of a WebResource. Remove the placed token first or Google may re-verify.",
773
- inputSchema: z.object({ id: z.string().describe("WebResource id (from list-verified-sites)") }).shape
774
- }, async ({ id }) => {
775
- await unverifySite(await getClient(), id);
776
- return { content: [{
777
- type: "text",
778
- text: JSON.stringify({
779
- id,
780
- status: "unverified"
781
- }, null, 2)
782
- }] };
783
- });
784
- server.registerTool("discover-sitemap", {
785
- description: "Probe a domain's robots.txt + common paths for an advertised sitemap (no auth).",
786
- inputSchema: z.object({ domain: z.string().describe("Domain (e.g., example.com)") }).shape
787
- }, async ({ domain }) => {
788
- const cleaned = String(domain).replace(/^https?:\/\//, "").replace(/\/.*$/, "");
789
- const url = await discoverSitemap(cleaned).catch(() => null);
790
- return { content: [{
791
- type: "text",
792
- text: JSON.stringify({
793
- domain: cleaned,
794
- sitemap: url
795
- }, null, 2)
796
- }] };
797
- });
798
- server.registerTool("batch-get-indexing-status", {
799
- description: "Get indexing notification metadata for multiple URLs.",
800
- inputSchema: z.object({
801
- urls: z.array(z.string()).describe("URLs"),
802
- delayMs: z.number().optional().describe("Delay between requests in ms (default 100)"),
803
- concurrency: z.number().optional().describe("Concurrent in-flight requests (default 1)")
804
- }).shape
805
- }, async ({ urls, delayMs, concurrency }) => {
806
- const client = await getClient();
807
- const results = await runSequentialBatch(urls, (url) => getIndexingMetadata(client, url), {
808
- delayMs: delayMs ?? 100,
809
- concurrency: concurrency ?? 1
810
- });
811
- return { content: [{
812
- type: "text",
813
- text: JSON.stringify(results, null, 2)
814
- }] };
815
- });
816
- return server;
817
- }
818
- async function checkAuth() {
819
- if (await resolveServiceAccount().then(Boolean).catch(() => false)) return { ok: true };
820
- if (resolveBYOK()) return { ok: true };
821
- const config = await loadConfig();
822
- if (!config.clientId && !config.clientSecret) return {
823
- ok: false,
824
- error: `GSCDump not configured.
825
-
826
- Run this command to set up authentication:
827
-
828
- npx @gscdump/cli init
829
-
830
- Or set BYOK env vars: GSC_ACCESS_TOKEN, or GSC_CLIENT_ID + GSC_CLIENT_SECRET + GSC_REFRESH_TOKEN
831
- (GOOGLE_* aliases also accepted).
832
-
833
- Then restart your MCP client.`
834
- };
835
- if (!await loadTokens()) return {
836
- ok: false,
837
- error: `Authentication missing.
838
-
839
- Run this command to authenticate:
840
-
841
- npx @gscdump/cli auth login
842
-
843
- Then restart your MCP client.`
844
- };
845
- return { ok: true };
846
- }
847
- const mcpCommand = defineCommand({
848
- meta: {
849
- name: "mcp",
850
- description: "Start MCP server for AI assistants"
851
- },
852
- async run() {
853
- const authCheck = await checkAuth();
854
- if (!authCheck.ok) {
855
- process.stderr.write(`\n${authCheck.error}\n\n`);
856
- process.exit(1);
857
- }
858
- const server = createGscMcpServer({
859
- name: "gscdump",
860
- version: VERSION,
861
- getAuth: () => resolveAuth({ interactive: false })
862
- });
863
- const transport = new StdioServerTransport();
864
- await server.connect(transport);
865
- }
866
- });
867
- export { mcpCommand };