@gscdump/cli 3.8.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +113 -30
  2. package/bin/gscdump.mjs +21 -1
  3. package/dist/analysis-local.mjs +172 -53
  4. package/dist/auth-state.mjs +21 -6
  5. package/dist/auth.mjs +27 -18
  6. package/dist/bing-data.mjs +6 -2
  7. package/dist/bing-hosted.mjs +1 -1
  8. package/dist/cli-args.mjs +8 -0
  9. package/dist/cli.d.mts +5 -1
  10. package/dist/cli.mjs +41 -10
  11. package/dist/cloud-google.mjs +8 -1
  12. package/dist/command-meta.mjs +4 -4
  13. package/dist/command-registry.mjs +86 -10
  14. package/dist/commands/analyze.mjs +84 -43
  15. package/dist/commands/auth.mjs +46 -37
  16. package/dist/commands/bing.mjs +3 -3
  17. package/dist/commands/compact.mjs +12 -8
  18. package/dist/commands/config.mjs +14 -14
  19. package/dist/commands/doctor.mjs +26 -26
  20. package/dist/commands/dump.mjs +203 -105
  21. package/dist/commands/entities.mjs +10 -113
  22. package/dist/commands/gc.mjs +4 -3
  23. package/dist/commands/indexing-urls.mjs +185 -0
  24. package/dist/commands/indexing.mjs +12 -8
  25. package/dist/commands/init.mjs +77 -18
  26. package/dist/commands/inspect.mjs +97 -110
  27. package/dist/commands/mcp.mjs +37 -57
  28. package/dist/commands/papercut.mjs +1 -1
  29. package/dist/commands/profile.mjs +9 -17
  30. package/dist/commands/query.mjs +219 -210
  31. package/dist/commands/report.mjs +68 -85
  32. package/dist/commands/rollups.mjs +9 -6
  33. package/dist/commands/sitemaps.mjs +51 -104
  34. package/dist/commands/sites.mjs +40 -24
  35. package/dist/commands/stats.mjs +14 -17
  36. package/dist/commands/store-purge.mjs +9 -5
  37. package/dist/commands/store.mjs +0 -12
  38. package/dist/commands/sync.mjs +774 -296
  39. package/dist/config.mjs +7 -4
  40. package/dist/context.mjs +51 -21
  41. package/dist/coverage.mjs +251 -0
  42. package/dist/dump-bing.mjs +71 -0
  43. package/dist/dump-writers.mjs +246 -0
  44. package/dist/error-handler.mjs +108 -38
  45. package/dist/filters.mjs +125 -0
  46. package/dist/hosted-site.mjs +91 -0
  47. package/dist/inspect-urls.mjs +76 -0
  48. package/dist/inspection-record.mjs +93 -0
  49. package/dist/local-entities.mjs +658 -0
  50. package/dist/mcp/errors.mjs +71 -18
  51. package/dist/mcp/handlers/diagnostics.mjs +15 -12
  52. package/dist/mcp/handlers/reports.mjs +33 -42
  53. package/dist/mcp/server/index.mjs +80 -38
  54. package/dist/mcp/types.mjs +7 -7
  55. package/dist/package.mjs +1 -1
  56. package/dist/quota-ledger.mjs +370 -0
  57. package/dist/render/analysis.mjs +8 -1
  58. package/dist/render/report.mjs +8 -1
  59. package/dist/request-pacer.mjs +29 -0
  60. package/dist/route.mjs +295 -0
  61. package/dist/sitemap.mjs +4 -0
  62. package/dist/sql-views.mjs +117 -0
  63. package/dist/store-sites.mjs +134 -0
  64. package/dist/sync-plan.mjs +129 -0
  65. package/dist/sync-run.mjs +96 -0
  66. package/dist/table-sources.mjs +57 -0
  67. package/dist/token-info.mjs +54 -0
  68. package/dist/utils.mjs +28 -19
  69. package/dist/window.mjs +159 -0
  70. package/package.json +17 -14
  71. package/skills/gscdump/SKILL.md +167 -37
  72. package/dist/commands/export.mjs +0 -76
  73. package/dist/native-duckdb.mjs +0 -46
@@ -1,22 +1,13 @@
1
- import { isQueryError } from "gscdump/query";
1
+ import { HOSTED_KEY_REJECTED, HOSTED_KEY_REJECTED_REASON } from "../error-handler.mjs";
2
+ import { resolveAuthentication } from "../auth-state.mjs";
3
+ import { resolveBYOK, resolveServiceAccount } from "../auth.mjs";
4
+ import { COMPARISON_FLAGS, PERIOD_FLAGS } from "../window.mjs";
2
5
  import { isAnalysisError } from "@gscdump/analysis/errors";
3
6
  import { isEngineError } from "@gscdump/engine/errors";
4
- const PERIODS = [
5
- "7d",
6
- "28d",
7
- "30d",
8
- "90d",
9
- "180d",
10
- "365d",
11
- "mtd",
12
- "ytd",
13
- "custom"
14
- ];
15
- const COMPARISONS = [
16
- "none",
17
- "prev-period",
18
- "yoy"
19
- ];
7
+ import { classifyError } from "gscdump/errors";
8
+ import { isQueryError } from "gscdump/query";
9
+ const PERIODS = PERIOD_FLAGS;
10
+ const COMPARISONS = COMPARISON_FLAGS;
20
11
  const mcpHandlerErrors = {
21
12
  unknownReport(id, available) {
22
13
  return {
@@ -50,6 +41,12 @@ const mcpHandlerErrors = {
50
41
  message: `Unknown comparison "${value}". Supported: ${COMPARISONS.join(", ")}.`
51
42
  };
52
43
  },
44
+ invalidWindow(message) {
45
+ return {
46
+ kind: "invalid-window",
47
+ message
48
+ };
49
+ },
53
50
  noValidDimension() {
54
51
  return {
55
52
  kind: "no-valid-dimension",
@@ -90,4 +87,60 @@ function asAnalysisError(error) {
90
87
  const tagged = error?.analysisError;
91
88
  return isAnalysisError(tagged) ? tagged : null;
92
89
  }
93
- export { enrichToolError, mcpHandlerErrorToException, mcpHandlerErrors };
90
+ function httpStatus(error) {
91
+ const value = error;
92
+ const status = value?.statusCode ?? value?.status ?? value?.response?.status;
93
+ return typeof status === "number" ? status : void 0;
94
+ }
95
+ async function authenticationMode() {
96
+ if ((await resolveAuthentication().catch(() => null))?._tag === "Cloud") return "cloud";
97
+ if (await resolveServiceAccount().then(Boolean).catch(() => null)) return "service-account";
98
+ return await Promise.resolve().then(() => resolveBYOK()).catch(() => null) ? "byok" : "local";
99
+ }
100
+ const OAUTH_TOKEN_FAILURE_RE = /invalid_grant|invalid_client|unauthorized_client|token has been expired|token has been revoked/i;
101
+ function classifyGoogleError(error) {
102
+ const classified = classifyError(error);
103
+ if (classified.kind !== "validation") return classified;
104
+ const text = `${classified.message} ${googleMessage(error) ?? ""}`;
105
+ return OAUTH_TOKEN_FAILURE_RE.test(text) ? {
106
+ ...classified,
107
+ kind: "auth-expired"
108
+ } : classified;
109
+ }
110
+ function nextStep(error, status, mode) {
111
+ if (error.kind === "rate-limited") return `Google quota or rate limit reached. Try again in ${error.retryAfter ? `${error.retryAfter}s` : "a few minutes"}.`;
112
+ if (status === 403) {
113
+ if (mode === "cloud") return "The gscdump.com account cannot open this Site. Check the Site is connected to your account at gscdump.com.";
114
+ return "The signed-in account cannot open this Site. Check its Search Console permissions, or run `gscdump auth status` to see the account.";
115
+ }
116
+ if (status === 401 || error.kind === "auth-expired") {
117
+ if (mode === "cloud") return "Set or refresh GSCDUMP_API_KEY to a user API key from your gscdump.com settings.";
118
+ if (mode === "service-account") return "Fix the service-account key (GSC_SERVICE_ACCOUNT_JSON or GOOGLE_APPLICATION_CREDENTIALS) in the MCP server configuration and restart the MCP client, or run `gscdump auth status`.";
119
+ if (mode === "byok") return "Refresh GSC_ACCESS_TOKEN (or GSC_CLIENT_ID, GSC_CLIENT_SECRET, and GSC_REFRESH_TOKEN) in the MCP server configuration and restart the MCP client.";
120
+ return "Run `gscdump auth login` in a terminal to connect again.";
121
+ }
122
+ if (error.kind === "not-found") return "Check the Site and URL. Call list-sites to see the Sites of this account.";
123
+ if (error.kind === "validation") return "Check the tool arguments.";
124
+ return status >= 500 ? "Try again later." : "";
125
+ }
126
+ function describeApiError(error, mode = "local") {
127
+ const status = httpStatus(error);
128
+ if (status === void 0) return null;
129
+ const classified = classifyGoogleError(error);
130
+ const message = googleMessage(error) ?? classified.message;
131
+ return `API error ${status}: ${(message === HOSTED_KEY_REJECTED ? HOSTED_KEY_REJECTED_REASON : message).replace(/\s+/g, " ").trim().replace(/\.$/, "")}. ${nextStep(classified, status, mode)}`.trim();
132
+ }
133
+ function googleMessage(error) {
134
+ const bodies = [error?.data, error?.response?.data];
135
+ for (const body of bodies) {
136
+ const data = body;
137
+ const nested = (data?.error)?.message;
138
+ const message = typeof nested === "string" ? nested : data?.error_description;
139
+ if (typeof message === "string" && message) return message;
140
+ }
141
+ }
142
+ async function toolErrorMessage(error) {
143
+ const mode = await authenticationMode();
144
+ return enrichToolError(error)?.message ?? describeApiError(error, mode) ?? (error instanceof Error ? error.message : String(error));
145
+ }
146
+ export { describeApiError, enrichToolError, mcpHandlerErrorToException, mcpHandlerErrors, toolErrorMessage };
@@ -1,5 +1,6 @@
1
1
  import { getCloudAccount } from "../../auth-state.mjs";
2
2
  import { missingRequiredScopes } from "../../auth-scopes.mjs";
3
+ import { currentAccessToken, fetchTokenInfo, redactTokens } from "../../token-info.mjs";
3
4
  import { ofetch } from "ofetch";
4
5
  const FETCH_TIMEOUT_MS = 5e3;
5
6
  const TIME_SKEW_WARN_MS = 3e5;
@@ -35,12 +36,12 @@ async function diagnostics(_input, ctx) {
35
36
  checks
36
37
  };
37
38
  }
38
- const token = await resolveAccessToken(ctx.auth);
39
- if (!token) {
39
+ const current = await resolveAccessToken(ctx.auth);
40
+ if (current.kind === "failed") {
40
41
  checks.push({
41
42
  name: "auth",
42
43
  status: "fail",
43
- detail: "no usable access token (refresh failed or no credentials)"
44
+ detail: `no usable access token: ${current.detail}`
44
45
  });
45
46
  return {
46
47
  ok: false,
@@ -48,24 +49,24 @@ async function diagnostics(_input, ctx) {
48
49
  };
49
50
  }
50
51
  const [tokenInfo, timeCheck, gscReachable, indexingReachable, sites] = await Promise.all([
51
- ofetch("https://oauth2.googleapis.com/tokeninfo", { query: { access_token: token } }).catch((e) => ({ error: e.message })),
52
+ fetchTokenInfo(current.token),
52
53
  probeTimeSkew(),
53
54
  probeReachable("https://searchconsole.googleapis.com/$discovery/rest?version=v1"),
54
55
  probeReachable("https://indexing.googleapis.com/$discovery/rest?version=v3"),
55
56
  ctx.client.sites().catch((e) => e)
56
57
  ]);
57
- if ("error" in tokenInfo) checks.push({
58
+ if (tokenInfo.kind === "invalid") checks.push({
58
59
  name: "auth",
59
60
  status: "fail",
60
- detail: `tokeninfo failed: ${tokenInfo.error}`
61
+ detail: `tokeninfo failed: ${tokenInfo.detail}`
61
62
  });
62
63
  else {
63
64
  checks.push({
64
65
  name: "auth",
65
66
  status: "pass",
66
- detail: tokenInfo.email ?? "token valid"
67
+ detail: tokenInfo.info.email ?? "token valid"
67
68
  });
68
- const scopes = tokenInfo.scope ? tokenInfo.scope.split(/\s+/) : [];
69
+ const scopes = tokenInfo.info.scope ? tokenInfo.info.scope.split(/\s+/) : [];
69
70
  const missing = missingRequiredScopes(scopes);
70
71
  checks.push(missing.length > 0 ? {
71
72
  name: "auth.scopes",
@@ -91,7 +92,7 @@ async function diagnostics(_input, ctx) {
91
92
  if (sites instanceof Error) checks.push({
92
93
  name: "gsc.sites",
93
94
  status: "fail",
94
- detail: `sites() failed: ${sites.message}`
95
+ detail: `sites() failed: ${redactTokens(sites.message, [current.token])}`
95
96
  });
96
97
  else {
97
98
  const verified = sites.filter((s) => s.permissionLevel !== "siteUnverifiedUser").length;
@@ -107,9 +108,11 @@ async function diagnostics(_input, ctx) {
107
108
  };
108
109
  }
109
110
  async function resolveAccessToken(auth) {
110
- if (typeof auth === "string") return auth;
111
- if (auth && typeof auth === "object" && "getAccessToken" in auth) return (await auth.getAccessToken().catch(() => null))?.token ?? null;
112
- return null;
111
+ if (typeof auth === "string" || auth && typeof auth === "object") return currentAccessToken(auth);
112
+ return {
113
+ kind: "failed",
114
+ detail: "no credentials"
115
+ };
113
116
  }
114
117
  async function probeTimeSkew() {
115
118
  const dateHeader = await ofetch.raw("https://oauth2.googleapis.com/tokeninfo", {
@@ -1,28 +1,25 @@
1
+ import { parseWindowFlags, windowFlagErrorToException } from "../../window.mjs";
1
2
  import { enrichToolError, mcpHandlerErrorToException, mcpHandlerErrors } from "../errors.mjs";
2
3
  import { runReportInput } from "../types.mjs";
4
+ import { getLatestGscDate } from "gscdump/dates";
5
+ import { err, ok, unwrapResult } from "gscdump/result";
3
6
  import { defaultAnalyzerRegistry } from "@gscdump/analysis/registry";
4
- import { resolveWindow } from "@gscdump/engine/period";
5
7
  import { GSC_API_CAPABILITIES, createGscApiQuerySource } from "@gscdump/engine-gsc-api";
6
- import { err, ok, unwrapResult } from "gscdump/result";
7
8
  import { defaultReportRegistry, runReport } from "@gscdump/analysis/report";
8
- const PERIOD_ALIASES = {
9
- "7d": "last-7d",
10
- "28d": "last-28d",
11
- "30d": "last-30d",
12
- "90d": "last-90d",
13
- "180d": "last-180d",
14
- "365d": "last-365d",
15
- "mtd": "mtd",
16
- "ytd": "ytd",
17
- "custom": "custom"
18
- };
19
- const COMPARISON_ALIASES = {
20
- "none": "none",
21
- "prev": "prev-period",
22
- "prev-period": "prev-period",
23
- "prior": "prev-period",
24
- "yoy": "yoy"
25
- };
9
+ function reportWindowDefaults(report) {
10
+ if (report.defaultPeriod === "custom") throw new Error(`Report "${report.id}" declares a custom default period. Declare a preset.`);
11
+ return {
12
+ preset: report.defaultPeriod,
13
+ comparison: report.defaultComparison
14
+ };
15
+ }
16
+ function toMcpWindowError(error) {
17
+ switch (error.kind) {
18
+ case "unknown-period": return mcpHandlerErrors.unknownPeriod(error.value);
19
+ case "unknown-comparison": return mcpHandlerErrors.unknownComparison(error.value);
20
+ default: return mcpHandlerErrors.invalidWindow(error.message);
21
+ }
22
+ }
26
23
  function mcpArgName(name) {
27
24
  return name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
28
25
  }
@@ -38,10 +35,7 @@ function supportsLiveReport(report) {
38
35
  const args = Object.entries(report.argsSpec);
39
36
  if (args.some(([name]) => !Object.hasOwn(runReportInput.shape, mcpArgName(name)))) return false;
40
37
  const params = Object.fromEntries(args.map(([name, arg]) => [mcpArgName(name), arg.default ?? (arg.type === "number" ? 1 : arg.type === "boolean" ? false : "mcp-discovery")]));
41
- const window = resolveWindow({
42
- preset: report.defaultPeriod,
43
- comparison: report.defaultComparison
44
- });
38
+ const window = unwrapResult(parseWindowFlags({}, reportWindowDefaults(report), getLatestGscDate()), windowFlagErrorToException);
45
39
  return supportsLiveSteps(report.plan(params, window));
46
40
  }
47
41
  function listReports() {
@@ -53,36 +47,33 @@ function listReports() {
53
47
  argsSpec: Object.fromEntries(Object.entries(r.argsSpec).map(([name, arg]) => [mcpArgName(name), arg]))
54
48
  }));
55
49
  }
56
- async function runReportHandlerResult(input, getContext) {
50
+ async function runReportHandlerResult(input, getContext, resolveSite = async (siteUrl) => siteUrl) {
57
51
  const report = defaultReportRegistry.getReport(input.id);
58
52
  if (!report) return err(mcpHandlerErrors.unknownReport(input.id, listReports().map((report) => report.id)));
59
53
  if (!supportsLiveReport(report)) return err(mcpHandlerErrors.unsupportedReport(input.id, listReports().map((report) => report.id)));
60
- const preset = input.period ? PERIOD_ALIASES[input.period.toLowerCase()] ?? null : report.defaultPeriod;
61
- if (!preset) return err(mcpHandlerErrors.unknownPeriod(input.period ?? ""));
62
- const comparison = input.comparison ? COMPARISON_ALIASES[input.comparison.toLowerCase()] ?? null : report.defaultComparison;
63
- if (!comparison) return err(mcpHandlerErrors.unknownComparison(input.comparison ?? ""));
64
- const window = resolveWindow({
65
- preset,
66
- comparison,
54
+ const parsed = parseWindowFlags({
55
+ period: input.period,
56
+ vs: input.comparison,
67
57
  start: input.start,
68
- end: input.end
69
- });
70
- if (input.prevStart && input.prevEnd) window.comparison = {
71
- start: input.prevStart,
72
- end: input.prevEnd
73
- };
58
+ end: input.end,
59
+ prevStart: input.prevStart,
60
+ prevEnd: input.prevEnd
61
+ }, reportWindowDefaults(report), getLatestGscDate());
62
+ if (!parsed.ok) return err(toMcpWindowError(parsed.error));
63
+ const window = parsed.value;
74
64
  const params = Object.fromEntries(Object.keys(report.argsSpec).map((name) => {
75
65
  const key = mcpArgName(name);
76
66
  return [key, input[key]];
77
67
  }).filter(([, value]) => value !== void 0));
78
68
  if (!supportsLiveSteps(report.plan(params, window))) return err(mcpHandlerErrors.unsupportedReport(input.id, listReports().map((report) => report.id)));
69
+ const siteUrl = await resolveSite(input.siteUrl);
79
70
  const ctx = await getContext();
80
71
  const source = createGscApiQuerySource({
81
72
  client: ctx.client,
82
- siteUrl: input.siteUrl
73
+ siteUrl
83
74
  });
84
75
  const reportCtx = {
85
- site: input.siteUrl,
76
+ site: siteUrl,
86
77
  window,
87
78
  params,
88
79
  registryVersion: defaultReportRegistry.version
@@ -93,8 +84,8 @@ async function runReportHandlerResult(input, getContext) {
93
84
  ctx: reportCtx
94
85
  }));
95
86
  }
96
- async function runReportHandler(input, getContext) {
97
- return unwrapResult(await runReportHandlerResult(input, getContext).catch((thrown) => {
87
+ async function runReportHandler(input, getContext, resolveSite) {
88
+ return unwrapResult(await runReportHandlerResult(input, getContext, resolveSite).catch((thrown) => {
98
89
  throw enrichToolError(thrown) ?? thrown;
99
90
  }), mcpHandlerErrorToException);
100
91
  }
@@ -1,12 +1,15 @@
1
+ import { formatSiteResolution } from "../../context.mjs";
1
2
  import { discoverLiveSitemap } from "../../sitemap.mjs";
3
+ import { toolErrorMessage } from "../errors.mjs";
2
4
  import { diagnostics } from "../handlers/diagnostics.mjs";
3
5
  import { batchInspectUrls as batchInspectUrls$1, batchRequestIndexing as batchRequestIndexing$1, getIndexingStatus, requestIndexing as requestIndexing$1 } from "../handlers/indexing.mjs";
4
- import { batchInspectUrlsInput, batchRequestIndexingInput, getIndexingStatusInput, inspectUrlInput, listReportsInput, listSitemapsInput, listSitesInput, requestIndexingInput, runReportInput, sitemapInput } from "../types.mjs";
6
+ import { batchInspectUrlsInput, batchRequestIndexingInput, getIndexingStatusInput, inspectUrlInput, listReportsInput, listSitemapsInput, listSitesInput, requestIndexingInput, runReportInput, siteUrlSchema, sitemapInput } from "../types.mjs";
5
7
  import { listReports, runReportHandler } from "../handlers/reports.mjs";
6
8
  import { getSitemap, listSitesWithSitemaps } from "../handlers/sites.mjs";
7
9
  import { z } from "zod";
8
10
  import { SearchTypes } from "gscdump/query";
9
11
  import { googleSearchConsole } from "gscdump/client";
12
+ import { resolveSiteInput } from "gscdump";
10
13
  import { getIndexingMetadata, runSequentialBatch } from "gscdump/indexing";
11
14
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
12
15
  import { addSite, deleteSite, getVerificationToken, getVerifiedSite, listVerifiedSites, unverifySite, verifySite } from "gscdump/sites";
@@ -60,6 +63,13 @@ function createGscMcpServer(options) {
60
63
  name,
61
64
  version
62
65
  });
66
+ const registerTool = ((toolName, config, callback) => server.registerTool(toolName, config, (...args) => Promise.resolve().then(() => callback(...args)).catch(async (error) => ({
67
+ isError: true,
68
+ content: [{
69
+ type: "text",
70
+ text: await toolErrorMessage(error)
71
+ }]
72
+ }))));
63
73
  const getContext = async () => {
64
74
  if (options.getContext) return options.getContext();
65
75
  const a = await options.getAuth();
@@ -69,20 +79,43 @@ function createGscMcpServer(options) {
69
79
  };
70
80
  };
71
81
  const getClient = async () => (await getContext()).client;
72
- server.registerTool("list-sites", {
82
+ let siteList;
83
+ const fetchSites = async () => {
84
+ return (await (await getClient()).sites()).flatMap((s) => s.siteUrl && s.permissionLevel !== "siteUnverifiedUser" ? [{
85
+ siteUrl: s.siteUrl,
86
+ permissionLevel: s.permissionLevel || "unknown"
87
+ }] : []);
88
+ };
89
+ const cacheSiteList = (sites) => {
90
+ const cached = sites.then((list) => list.map(({ siteUrl }) => ({ siteUrl }))).catch((error) => {
91
+ if (siteList === cached) siteList = void 0;
92
+ throw error;
93
+ });
94
+ siteList = cached;
95
+ return cached;
96
+ };
97
+ const loadSiteList = () => siteList ?? cacheSiteList(fetchSites());
98
+ const withSite = async (args) => {
99
+ const resolution = resolveSiteInput(args.siteUrl, await loadSiteList());
100
+ if (resolution.kind !== "resolved") throw new Error(formatSiteResolution(resolution, "account"));
101
+ return {
102
+ ...args,
103
+ siteUrl: resolution.siteUrl
104
+ };
105
+ };
106
+ registerTool("list-sites", {
73
107
  description: "List all Google Search Console sites visible to the authenticated user.",
74
108
  inputSchema: listSitesInput.shape
75
109
  }, async () => {
76
- const sites = (await (await getClient()).sites()).filter((s) => s.siteUrl && s.permissionLevel !== "siteUnverifiedUser").map((s) => ({
77
- siteUrl: s.siteUrl,
78
- permissionLevel: s.permissionLevel || "unknown"
79
- }));
110
+ const fetched = fetchSites();
111
+ cacheSiteList(fetched).catch(() => {});
112
+ const sites = await fetched;
80
113
  return { content: [{
81
114
  type: "text",
82
115
  text: JSON.stringify(sites, null, 2)
83
116
  }] };
84
117
  });
85
- server.registerTool("list-sites-with-sitemaps", {
118
+ registerTool("list-sites-with-sitemaps", {
86
119
  description: "List all GSC sites with their sitemaps",
87
120
  inputSchema: listSitesInput.shape
88
121
  }, async (args) => {
@@ -92,47 +125,50 @@ function createGscMcpServer(options) {
92
125
  text: JSON.stringify(result, null, 2)
93
126
  }] };
94
127
  });
95
- server.registerTool("list-sitemaps", {
128
+ registerTool("list-sitemaps", {
96
129
  description: "List sitemaps for a specific site",
97
130
  inputSchema: listSitemapsInput.shape
98
- }, async (args) => {
131
+ }, async (input) => {
132
+ const args = await withSite(input);
99
133
  const sitemaps = await (await getClient()).sitemaps.list(args.siteUrl);
100
134
  return { content: [{
101
135
  type: "text",
102
136
  text: JSON.stringify(sitemaps, null, 2)
103
137
  }] };
104
138
  });
105
- server.registerTool("get-sitemap", {
139
+ registerTool("get-sitemap", {
106
140
  description: "Get details for a specific sitemap",
107
141
  inputSchema: sitemapInput.shape
108
142
  }, async (args) => {
109
- const result = await getSitemap(args, await getContext());
143
+ const result = await getSitemap(await withSite(args), await getContext());
110
144
  return { content: [{
111
145
  type: "text",
112
146
  text: JSON.stringify(result, null, 2)
113
147
  }] };
114
148
  });
115
- server.registerTool("submit-sitemap", {
149
+ registerTool("submit-sitemap", {
116
150
  description: "Submit a sitemap to Google Search Console",
117
151
  inputSchema: sitemapInput.shape
118
- }, async (args) => {
152
+ }, async (input) => {
153
+ const args = await withSite(input);
119
154
  await (await getClient()).sitemaps.submit(args.siteUrl, args.feedpath);
120
155
  return { content: [{
121
156
  type: "text",
122
157
  text: JSON.stringify({ success: true }, null, 2)
123
158
  }] };
124
159
  });
125
- server.registerTool("delete-sitemap", {
160
+ registerTool("delete-sitemap", {
126
161
  description: "Delete a sitemap from Google Search Console",
127
162
  inputSchema: sitemapInput.shape
128
- }, async (args) => {
163
+ }, async (input) => {
164
+ const args = await withSite(input);
129
165
  await (await getClient()).sitemaps.delete(args.siteUrl, args.feedpath);
130
166
  return { content: [{
131
167
  type: "text",
132
168
  text: JSON.stringify({ success: true }, null, 2)
133
169
  }] };
134
170
  });
135
- server.registerTool("list-reports", {
171
+ registerTool("list-reports", {
136
172
  description: "List Reports supported by the live Google API. Returns defaults and argsSpec using run-report input names.",
137
173
  inputSchema: listReportsInput.shape
138
174
  }, async () => {
@@ -142,11 +178,11 @@ function createGscMcpServer(options) {
142
178
  text: JSON.stringify(result, null, 2)
143
179
  }] };
144
180
  });
145
- server.registerTool("run-report", {
181
+ registerTool("run-report", {
146
182
  description: "Run a report against the GSC API. Returns a structured ReportResult with bounded findings per section. See list-reports for ids.",
147
183
  inputSchema: runReportInput.shape
148
184
  }, async (args) => {
149
- const result = await runReportHandler(args, getContext);
185
+ const result = await runReportHandler(args, getContext, async (siteUrl) => (await withSite({ siteUrl })).siteUrl);
150
186
  return { content: [{
151
187
  type: "text",
152
188
  text: JSON.stringify(result, null, 2)
@@ -176,10 +212,10 @@ function createGscMcpServer(options) {
176
212
  groupType: z.enum(["and"]).optional().describe("Only \"and\" is supported; multiple groups do not implement OR"),
177
213
  filters: z.array(dimensionFilterSchema)
178
214
  });
179
- server.registerTool("query", {
215
+ registerTool("query", {
180
216
  description: "Run a custom search analytics query with dimension filters (regex/contains/equals). Multiple filter groups do not implement OR.",
181
217
  inputSchema: z.object({
182
- siteUrl: z.string().describe("GSC property URL (e.g., sc-domain:example.com)"),
218
+ siteUrl: siteUrlSchema,
183
219
  startDate: z.string().describe("Start date (YYYY-MM-DD)"),
184
220
  endDate: z.string().describe("End date (YYYY-MM-DD)"),
185
221
  dimensions: z.array(z.enum([
@@ -196,7 +232,8 @@ function createGscMcpServer(options) {
196
232
  aggregationType: z.enum(["byPage", "byProperty"]).optional().describe("Aggregation type"),
197
233
  dimensionFilterGroups: z.array(filterGroupSchema).optional().describe("Filter groups use \"and\" internally. Multiple groups do not implement OR.")
198
234
  }).shape
199
- }, async ({ siteUrl, startDate, endDate, dimensions, rowLimit, type, dataState, aggregationType, dimensionFilterGroups }) => {
235
+ }, async ({ siteUrl: input, startDate, endDate, dimensions, rowLimit, type, dataState, aggregationType, dimensionFilterGroups }) => {
236
+ const { siteUrl } = await withSite({ siteUrl: input });
200
237
  const result = await runMcpSearchAnalyticsQuery(await getClient(), {
201
238
  siteUrl,
202
239
  startDate,
@@ -213,17 +250,18 @@ function createGscMcpServer(options) {
213
250
  text: JSON.stringify(result, null, 2)
214
251
  }] };
215
252
  });
216
- server.registerTool("inspect-url", {
253
+ registerTool("inspect-url", {
217
254
  description: "Inspect a URL to check its indexing status in Google Search Console",
218
255
  inputSchema: inspectUrlInput.shape
219
- }, async (args) => {
256
+ }, async (input) => {
257
+ const args = await withSite(input);
220
258
  const result = await (await getClient()).inspect(args.siteUrl, args.inspectionUrl);
221
259
  return { content: [{
222
260
  type: "text",
223
261
  text: JSON.stringify(result, null, 2)
224
262
  }] };
225
263
  });
226
- server.registerTool("request-indexing", {
264
+ registerTool("request-indexing", {
227
265
  description: "Request Google to index or remove a URL via the Indexing API",
228
266
  inputSchema: requestIndexingInput.shape
229
267
  }, async (args) => {
@@ -233,7 +271,7 @@ function createGscMcpServer(options) {
233
271
  text: JSON.stringify(result, null, 2)
234
272
  }] };
235
273
  });
236
- server.registerTool("get-indexing-status", {
274
+ registerTool("get-indexing-status", {
237
275
  description: "Get indexing status metadata for a URL",
238
276
  inputSchema: getIndexingStatusInput.shape
239
277
  }, async (args) => {
@@ -243,7 +281,7 @@ function createGscMcpServer(options) {
243
281
  text: JSON.stringify(result, null, 2)
244
282
  }] };
245
283
  });
246
- server.registerTool("batch-request-indexing", {
284
+ registerTool("batch-request-indexing", {
247
285
  description: "Batch request indexing for multiple URLs with rate limiting",
248
286
  inputSchema: batchRequestIndexingInput.shape
249
287
  }, async (args) => {
@@ -253,17 +291,17 @@ function createGscMcpServer(options) {
253
291
  text: JSON.stringify(result, null, 2)
254
292
  }] };
255
293
  });
256
- server.registerTool("batch-inspect-urls", {
294
+ registerTool("batch-inspect-urls", {
257
295
  description: "Batch inspect multiple URLs to check their indexing status",
258
296
  inputSchema: batchInspectUrlsInput.shape
259
297
  }, async (args) => {
260
- const result = await batchInspectUrls$1(args, await getContext());
298
+ const result = await batchInspectUrls$1(await withSite(args), await getContext());
261
299
  return { content: [{
262
300
  type: "text",
263
301
  text: JSON.stringify(result, null, 2)
264
302
  }] };
265
303
  });
266
- server.registerTool("diagnostics", {
304
+ registerTool("diagnostics", {
267
305
  description: "Run health checks on the active GSC connection: auth/scopes, time skew, API reachability, sites count.",
268
306
  inputSchema: listSitesInput.shape
269
307
  }, async (args) => {
@@ -273,12 +311,13 @@ function createGscMcpServer(options) {
273
311
  text: JSON.stringify(result, null, 2)
274
312
  }] };
275
313
  });
276
- server.registerTool("add-site", {
314
+ registerTool("add-site", {
277
315
  description: "Register a property in Search Console (unverified). Verify ownership separately.",
278
316
  inputSchema: z.object({ siteUrl: z.string().describe("Property URL (https://example.com/ or sc-domain:example.com)") }).shape
279
317
  }, async ({ siteUrl }) => {
280
318
  const client = await getClient();
281
319
  await addSite(client, siteUrl);
320
+ siteList = void 0;
282
321
  return { content: [{
283
322
  type: "text",
284
323
  text: JSON.stringify({
@@ -288,12 +327,13 @@ function createGscMcpServer(options) {
288
327
  }, null, 2)
289
328
  }] };
290
329
  });
291
- server.registerTool("delete-site", {
330
+ registerTool("delete-site", {
292
331
  description: "Remove a property from Search Console.",
293
332
  inputSchema: z.object({ siteUrl: z.string().describe("Property URL") }).shape
294
333
  }, async ({ siteUrl }) => {
295
334
  const client = await getClient();
296
335
  await deleteSite(client, siteUrl);
336
+ siteList = void 0;
297
337
  return { content: [{
298
338
  type: "text",
299
339
  text: JSON.stringify({
@@ -310,7 +350,7 @@ function createGscMcpServer(options) {
310
350
  "ANALYTICS",
311
351
  "TAG_MANAGER"
312
352
  ]);
313
- server.registerTool("get-verification-token", {
353
+ registerTool("get-verification-token", {
314
354
  description: "Get a verification token to place on the site or in DNS.",
315
355
  inputSchema: z.object({
316
356
  siteUrl: z.string(),
@@ -329,7 +369,7 @@ function createGscMcpServer(options) {
329
369
  }, null, 2)
330
370
  }] };
331
371
  });
332
- server.registerTool("verify-site", {
372
+ registerTool("verify-site", {
333
373
  description: "Trigger Google to validate a placed verification token.",
334
374
  inputSchema: z.object({
335
375
  siteUrl: z.string(),
@@ -338,6 +378,7 @@ function createGscMcpServer(options) {
338
378
  }, async ({ siteUrl, method }) => {
339
379
  const client = await getClient();
340
380
  const resource = await verifySite(client, siteUrl, method);
381
+ siteList = void 0;
341
382
  return { content: [{
342
383
  type: "text",
343
384
  text: JSON.stringify({
@@ -347,7 +388,7 @@ function createGscMcpServer(options) {
347
388
  }, null, 2)
348
389
  }] };
349
390
  });
350
- server.registerTool("list-verified-sites", {
391
+ registerTool("list-verified-sites", {
351
392
  description: "List verified WebResources from the Site Verification API.",
352
393
  inputSchema: z.object({}).shape
353
394
  }, async () => {
@@ -358,7 +399,7 @@ function createGscMcpServer(options) {
358
399
  text: JSON.stringify(resources, null, 2)
359
400
  }] };
360
401
  });
361
- server.registerTool("get-verified-site", {
402
+ registerTool("get-verified-site", {
362
403
  description: "Fetch a single verified WebResource by id.",
363
404
  inputSchema: z.object({ id: z.string().describe("WebResource id (from list-verified-sites)") }).shape
364
405
  }, async ({ id }) => {
@@ -369,12 +410,13 @@ function createGscMcpServer(options) {
369
410
  text: JSON.stringify(resource, null, 2)
370
411
  }] };
371
412
  });
372
- server.registerTool("unverify-site", {
413
+ registerTool("unverify-site", {
373
414
  description: "Drop the calling user's verified ownership of a WebResource. Remove the placed token first or Google may re-verify.",
374
415
  inputSchema: z.object({ id: z.string().describe("WebResource id (from list-verified-sites)") }).shape
375
416
  }, async ({ id }) => {
376
417
  const client = await getClient();
377
418
  await unverifySite(client, id);
419
+ siteList = void 0;
378
420
  return { content: [{
379
421
  type: "text",
380
422
  text: JSON.stringify({
@@ -383,7 +425,7 @@ function createGscMcpServer(options) {
383
425
  }, null, 2)
384
426
  }] };
385
427
  });
386
- server.registerTool("discover-sitemap", {
428
+ registerTool("discover-sitemap", {
387
429
  description: "Probe a domain's robots.txt + common paths for an advertised sitemap (no auth).",
388
430
  inputSchema: z.object({ domain: z.string().describe("Domain (e.g., example.com)") }).shape
389
431
  }, async ({ domain }) => {
@@ -399,7 +441,7 @@ function createGscMcpServer(options) {
399
441
  }, null, 2)
400
442
  }] };
401
443
  });
402
- server.registerTool("batch-get-indexing-status", {
444
+ registerTool("batch-get-indexing-status", {
403
445
  description: "Get indexing notification metadata for multiple URLs.",
404
446
  inputSchema: z.object({
405
447
  urls: z.array(z.string()).describe("URLs"),
@@ -4,7 +4,7 @@ const periodSchema = z.object({
4
4
  start: z.string().describe("Start date (YYYY-MM-DD)"),
5
5
  end: z.string().describe("End date (YYYY-MM-DD)")
6
6
  }).describe("Date range for the query");
7
- const siteUrlSchema = z.string().describe("GSC property URL (e.g., sc-domain:example.com or https://example.com/)");
7
+ const siteUrlSchema = z.string().describe("Site, for example example.com. Scheme, www and case do not matter. The tool resolves it to the Search Console property.");
8
8
  const queryOptionsSchema = z.object({
9
9
  type: z.enum(SearchTypes).optional().describe("Data type"),
10
10
  dataState: z.enum(["final", "all"]).optional().describe("Data state: final (settled) or all (includes fresh)"),
@@ -69,12 +69,12 @@ const listReportsInput = z.object({});
69
69
  const runReportInput = z.object({
70
70
  siteUrl: siteUrlSchema,
71
71
  id: z.string().describe("Report ID supported by the live Google API. See list-reports."),
72
- period: z.string().optional().describe("Window: 7d|28d|30d|90d|180d|365d|mtd|ytd|custom (default per report)."),
73
- comparison: z.string().optional().describe("Comparison: none|prev-period|yoy (default per report)."),
74
- start: z.string().optional().describe("Custom window start (YYYY-MM-DD); requires period=custom."),
75
- end: z.string().optional().describe("Custom window end (YYYY-MM-DD); requires period=custom."),
76
- prevStart: z.string().optional().describe("Override comparison-window start."),
77
- prevEnd: z.string().optional().describe("Override comparison-window end."),
72
+ period: z.string().optional().describe("Window: 7d|28d|30d|90d|180d|365d|mtd|qtd|ytd|last-quarter|custom (default per report). Windows end on the newest final GSC date."),
73
+ comparison: z.string().optional().describe("Comparison: none|prev-period|yoy (default per report). yoy compares with the same weekdays 52 weeks earlier."),
74
+ start: z.string().optional().describe("Custom window start (YYYY-MM-DD). Implies period=custom."),
75
+ end: z.string().optional().describe("Custom window end (YYYY-MM-DD). Implies period=custom."),
76
+ prevStart: z.string().optional().describe("Override comparison-window start. Pass with prevEnd."),
77
+ prevEnd: z.string().optional().describe("Override comparison-window end. Pass with prevStart."),
78
78
  maxFindings: z.number().optional().describe("Cap findings per section (per-report default ~5)."),
79
79
  minClicksChange: z.number().optional().describe("Minimum absolute click change for movers findings."),
80
80
  target: z.string().optional().describe("Page URL or query for triage. This Report requires the local Store."),