@gscdump/cli 0.24.0 → 0.25.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 (2) hide show
  1. package/dist/index.mjs +705 -13
  2. package/package.json +6 -6
package/dist/index.mjs CHANGED
@@ -6,10 +6,9 @@ import fs, { readFile, readdir, rm } from "node:fs/promises";
6
6
  import path, { join } from "node:path";
7
7
  import { AnalyzerCapabilityError, createEngineQuerySource, runAnalyzerFromSource } from "@gscdump/analysis";
8
8
  import { createGscApiQuerySource } from "@gscdump/engine-gsc-api";
9
- import { decodeSiteId, normalizeSiteUrl } from "gscdump/tenant";
9
+ import { addSite, batchInspectUrls, batchRequestIndexing, createAuth, daysAgo, decodeSiteId, deleteSite, discoverSitemap, fetchSitemap, fetchSitemapUrls, fetchSitesWithSitemaps, formatErrorForCli, getDateRange, getIndexingMetadata, getVerificationToken, getVerifiedSite, googleSearchConsole, listVerifiedSites, normalizeSiteUrl, progressBar, requestIndexing, runSequentialBatch, siteUrlToVerificationSite, unverifySite, verificationMethodsFor, verifySite } from "gscdump";
10
10
  import os from "node:os";
11
11
  import { cancel, confirm, isCancel, multiselect, select, text } from "@clack/prompts";
12
- import { addSite, batchInspectUrls, batchRequestIndexing, createAuth, daysAgo, deleteSite, discoverSitemap, fetchSitemap, fetchSitemapUrls, fetchSitesWithSitemaps, formatErrorForCli, getDateRange, getIndexingMetadata, getVerificationToken, getVerifiedSite, googleSearchConsole, listVerifiedSites, progressBar, requestIndexing, runSequentialBatch, siteUrlToVerificationSite, unverifySite, verificationMethodsFor, verifySite } from "gscdump";
13
12
  import { createServer } from "node:http";
14
13
  import { JWT, OAuth2Client } from "google-auth-library";
15
14
  import { ofetch } from "ofetch";
@@ -23,8 +22,9 @@ import { allTables, inferTable } from "@gscdump/engine/schema";
23
22
  import { DuckDBInstance } from "@duckdb/node-api";
24
23
  import { sqlEscape } from "@gscdump/engine/sql";
25
24
  import { createEmptyTypesStore, createIndexingMetadataStore, createInspectionStore, createSitemapStore } from "@gscdump/engine/entities";
26
- import { createGscMcpServer } from "@gscdump/mcp/server";
27
25
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
26
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
27
+ import { z } from "zod";
28
28
  import { defaultReportRegistry, dryRunReport, formatReport, runReport } from "@gscdump/analysis/report";
29
29
  import { resolveWindow } from "@gscdump/engine/period";
30
30
  import { inferLegacyTier } from "@gscdump/engine";
@@ -122,7 +122,7 @@ function loadEnvFromCwd() {
122
122
  }
123
123
  return applied;
124
124
  }
125
- var version = "0.24.0";
125
+ var version = "0.25.0";
126
126
  const ALL_SEARCH_TYPES$1 = Object.values(SearchTypes);
127
127
  const VERSION = version;
128
128
  function noSubcommandSelected(parent, subNames) {
@@ -1659,7 +1659,7 @@ const AUTH_SUBCOMMANDS = [
1659
1659
  "refresh",
1660
1660
  "scopes"
1661
1661
  ];
1662
- const REQUIRED_SCOPES$1 = [
1662
+ const REQUIRED_SCOPES$2 = [
1663
1663
  "https://www.googleapis.com/auth/webmasters",
1664
1664
  "https://www.googleapis.com/auth/indexing",
1665
1665
  "https://www.googleapis.com/auth/siteverification"
@@ -1677,7 +1677,7 @@ async function resolveLiveAuthState() {
1677
1677
  const tokenInfo = liveToken ? await fetchTokenInfo(liveToken) : null;
1678
1678
  const scopes = tokenInfo?.scope ? tokenInfo.scope.split(/\s+/).filter(Boolean) : [];
1679
1679
  const has = (s) => scopes.includes(s) || scopes.includes(s.replace(".readonly", ""));
1680
- const missing = REQUIRED_SCOPES$1.filter((s) => !has(s));
1680
+ const missing = REQUIRED_SCOPES$2.filter((s) => !has(s));
1681
1681
  return {
1682
1682
  byok,
1683
1683
  tokens,
@@ -2147,13 +2147,13 @@ const configCommand = defineCommand({
2147
2147
  });
2148
2148
  }
2149
2149
  });
2150
- const REQUIRED_SCOPES = [
2150
+ const REQUIRED_SCOPES$1 = [
2151
2151
  "https://www.googleapis.com/auth/webmasters",
2152
2152
  "https://www.googleapis.com/auth/indexing",
2153
2153
  "https://www.googleapis.com/auth/siteverification"
2154
2154
  ];
2155
- const FETCH_TIMEOUT_MS = 5e3;
2156
- const TIME_SKEW_WARN_MS = 5 * 6e4;
2155
+ const FETCH_TIMEOUT_MS$1 = 5e3;
2156
+ const TIME_SKEW_WARN_MS$1 = 5 * 6e4;
2157
2157
  const WATERMARK_STALE_DAYS_WARN = 7;
2158
2158
  const RELEVANT_ENV_KEYS = [
2159
2159
  "GSC_ACCESS_TOKEN",
@@ -2279,7 +2279,7 @@ async function checkAuth$1(envKeys) {
2279
2279
  detail: info.email
2280
2280
  });
2281
2281
  const scopes = info.scope ? info.scope.split(/\s+/) : [];
2282
- const missing = REQUIRED_SCOPES.filter((s) => !scopes.includes(s) && !scopes.includes(s.replace(".readonly", "")));
2282
+ const missing = REQUIRED_SCOPES$1.filter((s) => !scopes.includes(s) && !scopes.includes(s.replace(".readonly", "")));
2283
2283
  if (missing.length > 0) checks.push({
2284
2284
  name: "auth.scopes",
2285
2285
  status: "warn",
@@ -2311,7 +2311,7 @@ async function checkAuth$1(envKeys) {
2311
2311
  async function checkTimeSkew() {
2312
2312
  const dateHeader = await ofetch.raw("https://oauth2.googleapis.com/tokeninfo", {
2313
2313
  method: "GET",
2314
- timeout: FETCH_TIMEOUT_MS
2314
+ timeout: FETCH_TIMEOUT_MS$1
2315
2315
  }).then((r) => r.headers.get("date")).catch((e) => e?.response?.headers?.get("date") ?? null);
2316
2316
  if (!dateHeader) return [{
2317
2317
  name: "time",
@@ -2326,7 +2326,7 @@ async function checkTimeSkew() {
2326
2326
  }];
2327
2327
  const skewMs = Date.now() - remoteMs;
2328
2328
  const human = `${skewMs >= 0 ? "+" : ""}${(skewMs / 1e3).toFixed(1)}s`;
2329
- if (Math.abs(skewMs) > TIME_SKEW_WARN_MS) return [{
2329
+ if (Math.abs(skewMs) > TIME_SKEW_WARN_MS$1) return [{
2330
2330
  name: "time",
2331
2331
  status: "warn",
2332
2332
  detail: `local clock ${human} off Google — OAuth refresh may reject; sync your clock`
@@ -2415,7 +2415,7 @@ async function checkStoreWatermarks() {
2415
2415
  async function checkApiReachable(name, url) {
2416
2416
  const reachable = await ofetch.raw(url, {
2417
2417
  method: "GET",
2418
- timeout: FETCH_TIMEOUT_MS
2418
+ timeout: FETCH_TIMEOUT_MS$1
2419
2419
  }).then(() => true).catch(() => false);
2420
2420
  return [{
2421
2421
  name,
@@ -3627,6 +3627,698 @@ const inspectCommand = defineCommand({
3627
3627
  printInspection(args.url, inspection);
3628
3628
  }
3629
3629
  });
3630
+ const REQUIRED_SCOPES = [
3631
+ "https://www.googleapis.com/auth/webmasters",
3632
+ "https://www.googleapis.com/auth/indexing",
3633
+ "https://www.googleapis.com/auth/siteverification"
3634
+ ];
3635
+ const FETCH_TIMEOUT_MS = 5e3;
3636
+ const TIME_SKEW_WARN_MS = 5 * 6e4;
3637
+ async function diagnostics(_input, ctx) {
3638
+ const checks = [];
3639
+ const token = await resolveAccessToken(ctx.auth);
3640
+ if (!token) {
3641
+ checks.push({
3642
+ name: "auth",
3643
+ status: "fail",
3644
+ detail: "no usable access token (refresh failed or no credentials)"
3645
+ });
3646
+ return {
3647
+ ok: false,
3648
+ checks
3649
+ };
3650
+ }
3651
+ const [tokenInfo, timeCheck, gscReachable, indexingReachable, sites] = await Promise.all([
3652
+ ofetch("https://oauth2.googleapis.com/tokeninfo", { query: { access_token: token } }).catch((e) => ({ error: e.message })),
3653
+ probeTimeSkew(),
3654
+ probeReachable("https://searchconsole.googleapis.com/$discovery/rest?version=v1"),
3655
+ probeReachable("https://indexing.googleapis.com/$discovery/rest?version=v3"),
3656
+ ctx.client.sites().catch((e) => e)
3657
+ ]);
3658
+ if ("error" in tokenInfo) checks.push({
3659
+ name: "auth",
3660
+ status: "fail",
3661
+ detail: `tokeninfo failed: ${tokenInfo.error}`
3662
+ });
3663
+ else {
3664
+ checks.push({
3665
+ name: "auth",
3666
+ status: "pass",
3667
+ detail: tokenInfo.email ?? "token valid"
3668
+ });
3669
+ const scopes = tokenInfo.scope ? tokenInfo.scope.split(/\s+/) : [];
3670
+ const missing = REQUIRED_SCOPES.filter((s) => !scopes.includes(s));
3671
+ checks.push(missing.length > 0 ? {
3672
+ name: "auth.scopes",
3673
+ status: "warn",
3674
+ detail: `missing: ${missing.join(", ")}`
3675
+ } : {
3676
+ name: "auth.scopes",
3677
+ status: "pass",
3678
+ detail: `${scopes.length} granted`
3679
+ });
3680
+ }
3681
+ checks.push(timeCheck);
3682
+ checks.push({
3683
+ name: "gsc.api",
3684
+ status: gscReachable ? "pass" : "warn",
3685
+ detail: gscReachable ? "reachable" : "searchconsole.googleapis.com unreachable"
3686
+ });
3687
+ checks.push({
3688
+ name: "indexing.api",
3689
+ status: indexingReachable ? "pass" : "warn",
3690
+ detail: indexingReachable ? "reachable" : "indexing.googleapis.com unreachable"
3691
+ });
3692
+ if (sites instanceof Error) checks.push({
3693
+ name: "gsc.sites",
3694
+ status: "fail",
3695
+ detail: `sites() failed: ${sites.message}`
3696
+ });
3697
+ else {
3698
+ const verified = sites.filter((s) => s.permissionLevel !== "siteUnverifiedUser").length;
3699
+ checks.push({
3700
+ name: "gsc.sites",
3701
+ status: "pass",
3702
+ detail: `${sites.length} site(s) accessible (${verified} verified)`
3703
+ });
3704
+ }
3705
+ return {
3706
+ ok: !checks.some((c) => c.status === "fail"),
3707
+ checks
3708
+ };
3709
+ }
3710
+ async function resolveAccessToken(auth) {
3711
+ if (typeof auth === "string") return auth;
3712
+ if (auth && typeof auth === "object" && "getAccessToken" in auth) return (await auth.getAccessToken().catch(() => null))?.token ?? null;
3713
+ return null;
3714
+ }
3715
+ async function probeTimeSkew() {
3716
+ const dateHeader = await ofetch.raw("https://oauth2.googleapis.com/tokeninfo", {
3717
+ method: "GET",
3718
+ timeout: FETCH_TIMEOUT_MS
3719
+ }).then((r) => r.headers.get("date")).catch((e) => e?.response?.headers?.get("date") ?? null);
3720
+ if (!dateHeader) return {
3721
+ name: "time",
3722
+ status: "warn",
3723
+ detail: "no Date header"
3724
+ };
3725
+ const remoteMs = Date.parse(dateHeader);
3726
+ if (!Number.isFinite(remoteMs)) return {
3727
+ name: "time",
3728
+ status: "warn",
3729
+ detail: `unparseable Date header: ${dateHeader}`
3730
+ };
3731
+ const skewMs = Date.now() - remoteMs;
3732
+ const human = `${skewMs >= 0 ? "+" : ""}${(skewMs / 1e3).toFixed(1)}s`;
3733
+ if (Math.abs(skewMs) > TIME_SKEW_WARN_MS) return {
3734
+ name: "time",
3735
+ status: "warn",
3736
+ detail: `local clock ${human} off Google`
3737
+ };
3738
+ return {
3739
+ name: "time",
3740
+ status: "pass",
3741
+ detail: `in sync (${human})`
3742
+ };
3743
+ }
3744
+ async function probeReachable(url) {
3745
+ return ofetch.raw(url, {
3746
+ method: "GET",
3747
+ timeout: FETCH_TIMEOUT_MS
3748
+ }).then(() => true).catch(() => false);
3749
+ }
3750
+ async function requestIndexing$1(input, ctx) {
3751
+ return requestIndexing(ctx.client, input.url, { type: input.type || "URL_UPDATED" }).catch((e) => ({
3752
+ url: input.url,
3753
+ type: input.type || "URL_UPDATED",
3754
+ error: e.message
3755
+ }));
3756
+ }
3757
+ async function getIndexingStatus(input, ctx) {
3758
+ return getIndexingMetadata(ctx.client, input.url).catch((e) => ({
3759
+ url: input.url,
3760
+ error: e.message
3761
+ }));
3762
+ }
3763
+ async function batchRequestIndexing$1(input, ctx) {
3764
+ const results = await batchRequestIndexing(ctx.client, input.urls, {
3765
+ type: input.type || "URL_UPDATED",
3766
+ delayMs: input.delayMs || 100
3767
+ });
3768
+ return {
3769
+ results,
3770
+ success: results.length,
3771
+ failed: 0
3772
+ };
3773
+ }
3774
+ async function batchInspectUrls$1(input, ctx) {
3775
+ const results = await batchInspectUrls(ctx.client, input.siteUrl, input.urls, { delayMs: input.delayMs || 200 });
3776
+ return {
3777
+ results,
3778
+ indexed: results.filter((r) => r.isIndexed).length,
3779
+ notIndexed: results.filter((r) => !r.isIndexed).length
3780
+ };
3781
+ }
3782
+ const PERIOD_ALIASES$1 = {
3783
+ "7d": "last-7d",
3784
+ "28d": "last-28d",
3785
+ "30d": "last-30d",
3786
+ "90d": "last-90d",
3787
+ "180d": "last-180d",
3788
+ "365d": "last-365d",
3789
+ "mtd": "mtd",
3790
+ "ytd": "ytd",
3791
+ "custom": "custom"
3792
+ };
3793
+ const COMPARISON_ALIASES$1 = {
3794
+ "none": "none",
3795
+ "prev": "prev-period",
3796
+ "prev-period": "prev-period",
3797
+ "prior": "prev-period",
3798
+ "yoy": "yoy"
3799
+ };
3800
+ function listReports() {
3801
+ return defaultReportRegistry.listReports().map((r) => ({
3802
+ id: r.id,
3803
+ description: r.description,
3804
+ defaultPeriod: r.defaultPeriod,
3805
+ defaultComparison: r.defaultComparison,
3806
+ argsSpec: r.argsSpec
3807
+ }));
3808
+ }
3809
+ async function runReportHandler(input, ctx) {
3810
+ const report = defaultReportRegistry.getReport(input.id);
3811
+ if (!report) throw new Error(`Unknown report id "${input.id}". Available: ${defaultReportRegistry.listReportIds().join(", ")}`);
3812
+ const preset = input.period ? PERIOD_ALIASES$1[input.period.toLowerCase()] ?? null : report.defaultPeriod;
3813
+ if (!preset) throw new Error(`Unknown period "${input.period}". Supported: 7d, 28d, 30d, 90d, 180d, 365d, mtd, ytd, custom.`);
3814
+ const comparison = input.comparison ? COMPARISON_ALIASES$1[input.comparison.toLowerCase()] ?? null : report.defaultComparison;
3815
+ if (!comparison) throw new Error(`Unknown comparison "${input.comparison}". Supported: none, prev-period, yoy.`);
3816
+ const window = resolveWindow({
3817
+ preset,
3818
+ comparison,
3819
+ start: input.start,
3820
+ end: input.end
3821
+ });
3822
+ if (input.prevStart && input.prevEnd) window.comparison = {
3823
+ start: input.prevStart,
3824
+ end: input.prevEnd
3825
+ };
3826
+ const params = {};
3827
+ if (input.maxFindings != null) params.maxFindings = input.maxFindings;
3828
+ return runReport(report, {
3829
+ source: createGscApiQuerySource({
3830
+ client: ctx.client,
3831
+ siteUrl: input.siteUrl
3832
+ }),
3833
+ analyzers: defaultAnalyzerRegistry,
3834
+ ctx: {
3835
+ site: input.siteUrl,
3836
+ window,
3837
+ params,
3838
+ registryVersion: defaultReportRegistry.version
3839
+ }
3840
+ });
3841
+ }
3842
+ async function listSitesWithSitemaps(_input, ctx) {
3843
+ return fetchSitesWithSitemaps(ctx.client);
3844
+ }
3845
+ async function getSitemap(input, ctx) {
3846
+ return fetchSitemap(ctx.client, input.siteUrl, input.feedpath);
3847
+ }
3848
+ const periodSchema = z.object({
3849
+ start: z.string().describe("Start date (YYYY-MM-DD)"),
3850
+ end: z.string().describe("End date (YYYY-MM-DD)")
3851
+ }).describe("Date range for the query");
3852
+ const siteUrlSchema = z.string().describe("GSC property URL (e.g., sc-domain:example.com or https://example.com/)");
3853
+ const queryOptionsSchema = z.object({
3854
+ type: z.enum([
3855
+ "web",
3856
+ "image",
3857
+ "video",
3858
+ "news",
3859
+ "discover",
3860
+ "googleNews"
3861
+ ]).optional().describe("Data type"),
3862
+ dataState: z.enum(["final", "all"]).optional().describe("Data state: final (settled) or all (includes fresh)"),
3863
+ aggregationType: z.enum(["byPage", "byProperty"]).optional().describe("Aggregation: byPage or byProperty")
3864
+ }).optional();
3865
+ const listSitesInput = z.object({});
3866
+ const listSitemapsInput = z.object({ siteUrl: siteUrlSchema });
3867
+ z.object({
3868
+ siteUrl: siteUrlSchema,
3869
+ period: periodSchema,
3870
+ comparePrevious: z.boolean().optional().describe("Include previous period comparison"),
3871
+ options: queryOptionsSchema
3872
+ });
3873
+ z.object({
3874
+ siteUrl: siteUrlSchema,
3875
+ period: periodSchema,
3876
+ url: z.string().describe("Page URL to fetch details for")
3877
+ });
3878
+ z.object({
3879
+ siteUrl: siteUrlSchema,
3880
+ period: periodSchema,
3881
+ keyword: z.string().describe("Keyword to fetch details for")
3882
+ });
3883
+ const inspectUrlInput = z.object({
3884
+ siteUrl: siteUrlSchema,
3885
+ inspectionUrl: z.string().describe("URL to inspect")
3886
+ });
3887
+ const requestIndexingInput = z.object({
3888
+ url: z.string().describe("URL to request indexing for"),
3889
+ type: z.enum(["URL_UPDATED", "URL_DELETED"]).optional().describe("Notification type")
3890
+ });
3891
+ const getIndexingStatusInput = z.object({ url: z.string().describe("URL to get indexing status for") });
3892
+ z.object({
3893
+ siteUrl: siteUrlSchema,
3894
+ period: periodSchema,
3895
+ dimensions: z.array(z.enum([
3896
+ "date",
3897
+ "query",
3898
+ "page",
3899
+ "country",
3900
+ "device",
3901
+ "searchAppearance"
3902
+ ])).describe("Dimensions to group by"),
3903
+ rowLimit: z.number().optional().describe("Max rows (default 25000)"),
3904
+ options: queryOptionsSchema
3905
+ });
3906
+ const sitemapInput = z.object({
3907
+ siteUrl: siteUrlSchema,
3908
+ feedpath: z.string().describe("Sitemap URL (e.g., https://example.com/sitemap.xml)")
3909
+ });
3910
+ const batchRequestIndexingInput = z.object({
3911
+ urls: z.array(z.string()).describe("URLs to request indexing for"),
3912
+ type: z.enum(["URL_UPDATED", "URL_DELETED"]).optional().describe("Notification type"),
3913
+ delayMs: z.number().optional().describe("Delay between requests in ms (default 100)")
3914
+ });
3915
+ const batchInspectUrlsInput = z.object({
3916
+ siteUrl: siteUrlSchema,
3917
+ urls: z.array(z.string()).describe("URLs to inspect"),
3918
+ delayMs: z.number().optional().describe("Delay between requests in ms (default 200)")
3919
+ });
3920
+ const listReportsInput = z.object({});
3921
+ const runReportInput = z.object({
3922
+ siteUrl: siteUrlSchema,
3923
+ id: z.string().describe("Report id (e.g. health, movers, opportunities, risks). See list-reports."),
3924
+ period: z.string().optional().describe("Window: 7d|28d|30d|90d|180d|365d|mtd|ytd|custom (default per report)."),
3925
+ comparison: z.string().optional().describe("Comparison: none|prev-period|yoy (default per report)."),
3926
+ start: z.string().optional().describe("Custom window start (YYYY-MM-DD); requires period=custom."),
3927
+ end: z.string().optional().describe("Custom window end (YYYY-MM-DD); requires period=custom."),
3928
+ prevStart: z.string().optional().describe("Override comparison-window start."),
3929
+ prevEnd: z.string().optional().describe("Override comparison-window end."),
3930
+ maxFindings: z.number().optional().describe("Cap findings per section (per-report default ~5).")
3931
+ });
3932
+ function createGscMcpServer(options) {
3933
+ const { name = "gscdump", version = "1.0.0", getAuth } = options;
3934
+ const server = new McpServer({
3935
+ name,
3936
+ version
3937
+ });
3938
+ const auth = async () => Promise.resolve(getAuth());
3939
+ const getContext = async () => {
3940
+ const a = await auth();
3941
+ return {
3942
+ auth: a,
3943
+ client: googleSearchConsole(a)
3944
+ };
3945
+ };
3946
+ const getClient = async () => googleSearchConsole(await auth());
3947
+ server.registerTool("list-sites", {
3948
+ description: "List all Google Search Console sites visible to the authenticated user.",
3949
+ inputSchema: listSitesInput.shape
3950
+ }, async () => {
3951
+ const sites = (await (await getClient()).sites()).filter((s) => s.siteUrl && s.permissionLevel !== "siteUnverifiedUser").map((s) => ({
3952
+ siteUrl: s.siteUrl,
3953
+ permissionLevel: s.permissionLevel || "unknown"
3954
+ }));
3955
+ return { content: [{
3956
+ type: "text",
3957
+ text: JSON.stringify(sites, null, 2)
3958
+ }] };
3959
+ });
3960
+ server.registerTool("list-sites-with-sitemaps", {
3961
+ description: "List all GSC sites with their sitemaps",
3962
+ inputSchema: listSitesInput.shape
3963
+ }, async (args) => {
3964
+ const result = await listSitesWithSitemaps(args, await getContext());
3965
+ return { content: [{
3966
+ type: "text",
3967
+ text: JSON.stringify(result, null, 2)
3968
+ }] };
3969
+ });
3970
+ server.registerTool("list-sitemaps", {
3971
+ description: "List sitemaps for a specific site",
3972
+ inputSchema: listSitemapsInput.shape
3973
+ }, async (args) => {
3974
+ const sitemaps = await (await getClient()).sitemaps.list(args.siteUrl);
3975
+ return { content: [{
3976
+ type: "text",
3977
+ text: JSON.stringify(sitemaps, null, 2)
3978
+ }] };
3979
+ });
3980
+ server.registerTool("get-sitemap", {
3981
+ description: "Get details for a specific sitemap",
3982
+ inputSchema: sitemapInput.shape
3983
+ }, async (args) => {
3984
+ const result = await getSitemap(args, await getContext());
3985
+ return { content: [{
3986
+ type: "text",
3987
+ text: JSON.stringify(result, null, 2)
3988
+ }] };
3989
+ });
3990
+ server.registerTool("submit-sitemap", {
3991
+ description: "Submit a sitemap to Google Search Console",
3992
+ inputSchema: sitemapInput.shape
3993
+ }, async (args) => {
3994
+ await (await getClient()).sitemaps.submit(args.siteUrl, args.feedpath);
3995
+ return { content: [{
3996
+ type: "text",
3997
+ text: JSON.stringify({ success: true }, null, 2)
3998
+ }] };
3999
+ });
4000
+ server.registerTool("delete-sitemap", {
4001
+ description: "Delete a sitemap from Google Search Console",
4002
+ inputSchema: sitemapInput.shape
4003
+ }, async (args) => {
4004
+ await (await getClient()).sitemaps.delete(args.siteUrl, args.feedpath);
4005
+ return { content: [{
4006
+ type: "text",
4007
+ text: JSON.stringify({ success: true }, null, 2)
4008
+ }] };
4009
+ });
4010
+ server.registerTool("list-reports", {
4011
+ description: "List available reports (intent-keyed analyzer compositions). Returns id, description, default period/comparison, and per-report argsSpec.",
4012
+ inputSchema: listReportsInput.shape
4013
+ }, async () => {
4014
+ const result = listReports();
4015
+ return { content: [{
4016
+ type: "text",
4017
+ text: JSON.stringify(result, null, 2)
4018
+ }] };
4019
+ });
4020
+ server.registerTool("run-report", {
4021
+ description: "Run a report against the GSC API. Returns a structured ReportResult with bounded findings per section. See list-reports for ids.",
4022
+ inputSchema: runReportInput.shape
4023
+ }, async (args) => {
4024
+ const result = await runReportHandler(args, await getContext());
4025
+ return { content: [{
4026
+ type: "text",
4027
+ text: JSON.stringify(result, null, 2)
4028
+ }] };
4029
+ });
4030
+ const filterOperatorSchema = z.enum([
4031
+ "equals",
4032
+ "notEquals",
4033
+ "contains",
4034
+ "notContains",
4035
+ "includingRegex",
4036
+ "excludingRegex"
4037
+ ]);
4038
+ const dimensionFilterSchema = z.object({
4039
+ dimension: z.enum([
4040
+ "date",
4041
+ "query",
4042
+ "page",
4043
+ "country",
4044
+ "device",
4045
+ "searchAppearance"
4046
+ ]),
4047
+ operator: filterOperatorSchema,
4048
+ expression: z.string()
4049
+ });
4050
+ const filterGroupSchema = z.object({
4051
+ groupType: z.enum(["and"]).optional().describe("Always \"and\"; multiple groups are OR-ed together"),
4052
+ filters: z.array(dimensionFilterSchema)
4053
+ });
4054
+ server.registerTool("query", {
4055
+ description: "Run a custom search analytics query. Supports dimension filters (regex/contains/equals) via dimensionFilterGroups; multiple groups are OR-ed.",
4056
+ inputSchema: z.object({
4057
+ siteUrl: z.string().describe("GSC property URL (e.g., sc-domain:example.com)"),
4058
+ startDate: z.string().describe("Start date (YYYY-MM-DD)"),
4059
+ endDate: z.string().describe("End date (YYYY-MM-DD)"),
4060
+ dimensions: z.array(z.enum([
4061
+ "date",
4062
+ "query",
4063
+ "page",
4064
+ "country",
4065
+ "device",
4066
+ "searchAppearance"
4067
+ ])).describe("Dimensions to group by"),
4068
+ rowLimit: z.number().optional().describe("Max rows (default 25000)"),
4069
+ type: z.enum([
4070
+ "web",
4071
+ "image",
4072
+ "video",
4073
+ "news",
4074
+ "discover",
4075
+ "googleNews"
4076
+ ]).optional().describe("Search type"),
4077
+ dataState: z.enum(["final", "all"]).optional().describe("Data state: final (settled) or all (includes fresh)"),
4078
+ aggregationType: z.enum(["byPage", "byProperty"]).optional().describe("Aggregation type"),
4079
+ dimensionFilterGroups: z.array(filterGroupSchema).optional().describe("Filter groups (each \"and\"-ed internally; multiple groups are OR-ed)")
4080
+ }).shape
4081
+ }, async ({ siteUrl, startDate, endDate, dimensions, rowLimit, type, dataState, aggregationType, dimensionFilterGroups }) => {
4082
+ const client = await getClient();
4083
+ const limit = rowLimit ?? 25e3;
4084
+ const allRows = [];
4085
+ let startRow = 0;
4086
+ while (true) {
4087
+ const rows = ((await client._rawQuery(siteUrl, {
4088
+ startDate,
4089
+ endDate,
4090
+ dimensions,
4091
+ rowLimit: limit,
4092
+ startRow,
4093
+ ...type ? { type } : {},
4094
+ ...dataState ? { dataState } : {},
4095
+ ...aggregationType ? { aggregationType } : {},
4096
+ ...dimensionFilterGroups ? { dimensionFilterGroups: dimensionFilterGroups.map((g) => ({
4097
+ groupType: g.groupType ?? "and",
4098
+ filters: g.filters
4099
+ })) } : {}
4100
+ })).rows || []).map((row) => {
4101
+ const result = {
4102
+ clicks: row.clicks ?? 0,
4103
+ impressions: row.impressions ?? 0,
4104
+ ctr: row.ctr ?? 0,
4105
+ position: row.position ?? 0
4106
+ };
4107
+ dimensions.forEach((dim, i) => {
4108
+ result[dim] = row.keys?.[i];
4109
+ });
4110
+ return result;
4111
+ });
4112
+ allRows.push(...rows);
4113
+ if (rows.length < limit) break;
4114
+ startRow += rows.length;
4115
+ }
4116
+ return { content: [{
4117
+ type: "text",
4118
+ text: JSON.stringify({
4119
+ siteUrl,
4120
+ rowCount: allRows.length,
4121
+ rows: allRows
4122
+ }, null, 2)
4123
+ }] };
4124
+ });
4125
+ server.registerTool("inspect-url", {
4126
+ description: "Inspect a URL to check its indexing status in Google Search Console",
4127
+ inputSchema: inspectUrlInput.shape
4128
+ }, async (args) => {
4129
+ const result = await (await getClient()).inspect(args.siteUrl, args.inspectionUrl);
4130
+ return { content: [{
4131
+ type: "text",
4132
+ text: JSON.stringify(result, null, 2)
4133
+ }] };
4134
+ });
4135
+ server.registerTool("request-indexing", {
4136
+ description: "Request Google to index or remove a URL via the Indexing API",
4137
+ inputSchema: requestIndexingInput.shape
4138
+ }, async (args) => {
4139
+ const result = await requestIndexing$1(args, await getContext());
4140
+ return { content: [{
4141
+ type: "text",
4142
+ text: JSON.stringify(result, null, 2)
4143
+ }] };
4144
+ });
4145
+ server.registerTool("get-indexing-status", {
4146
+ description: "Get indexing status metadata for a URL",
4147
+ inputSchema: getIndexingStatusInput.shape
4148
+ }, async (args) => {
4149
+ const result = await getIndexingStatus(args, await getContext());
4150
+ return { content: [{
4151
+ type: "text",
4152
+ text: JSON.stringify(result, null, 2)
4153
+ }] };
4154
+ });
4155
+ server.registerTool("batch-request-indexing", {
4156
+ description: "Batch request indexing for multiple URLs with rate limiting",
4157
+ inputSchema: batchRequestIndexingInput.shape
4158
+ }, async (args) => {
4159
+ const result = await batchRequestIndexing$1(args, await getContext());
4160
+ return { content: [{
4161
+ type: "text",
4162
+ text: JSON.stringify(result, null, 2)
4163
+ }] };
4164
+ });
4165
+ server.registerTool("batch-inspect-urls", {
4166
+ description: "Batch inspect multiple URLs to check their indexing status",
4167
+ inputSchema: batchInspectUrlsInput.shape
4168
+ }, async (args) => {
4169
+ const result = await batchInspectUrls$1(args, await getContext());
4170
+ return { content: [{
4171
+ type: "text",
4172
+ text: JSON.stringify(result, null, 2)
4173
+ }] };
4174
+ });
4175
+ server.registerTool("diagnostics", {
4176
+ description: "Run health checks on the active GSC connection: auth/scopes, time skew, API reachability, sites count.",
4177
+ inputSchema: listSitesInput.shape
4178
+ }, async (args) => {
4179
+ const result = await diagnostics(args, await getContext());
4180
+ return { content: [{
4181
+ type: "text",
4182
+ text: JSON.stringify(result, null, 2)
4183
+ }] };
4184
+ });
4185
+ server.registerTool("add-site", {
4186
+ description: "Register a property in Search Console (unverified). Verify ownership separately.",
4187
+ inputSchema: z.object({ siteUrl: z.string().describe("Property URL (https://example.com/ or sc-domain:example.com)") }).shape
4188
+ }, async ({ siteUrl }) => {
4189
+ await addSite(await getClient(), siteUrl);
4190
+ return { content: [{
4191
+ type: "text",
4192
+ text: JSON.stringify({
4193
+ siteUrl,
4194
+ status: "added",
4195
+ verified: false
4196
+ }, null, 2)
4197
+ }] };
4198
+ });
4199
+ server.registerTool("delete-site", {
4200
+ description: "Remove a property from Search Console.",
4201
+ inputSchema: z.object({ siteUrl: z.string().describe("Property URL") }).shape
4202
+ }, async ({ siteUrl }) => {
4203
+ await deleteSite(await getClient(), siteUrl);
4204
+ return { content: [{
4205
+ type: "text",
4206
+ text: JSON.stringify({
4207
+ siteUrl,
4208
+ status: "deleted"
4209
+ }, null, 2)
4210
+ }] };
4211
+ });
4212
+ const verificationMethodSchema = z.enum([
4213
+ "META",
4214
+ "FILE",
4215
+ "DNS_TXT",
4216
+ "DNS_CNAME",
4217
+ "ANALYTICS",
4218
+ "TAG_MANAGER"
4219
+ ]);
4220
+ server.registerTool("get-verification-token", {
4221
+ description: "Get a verification token to place on the site or in DNS.",
4222
+ inputSchema: z.object({
4223
+ siteUrl: z.string(),
4224
+ method: verificationMethodSchema
4225
+ }).shape
4226
+ }, async ({ siteUrl, method }) => {
4227
+ const result = await getVerificationToken(await getClient(), siteUrl, method);
4228
+ return { content: [{
4229
+ type: "text",
4230
+ text: JSON.stringify({
4231
+ siteUrl,
4232
+ method,
4233
+ token: result.token,
4234
+ site: result.site
4235
+ }, null, 2)
4236
+ }] };
4237
+ });
4238
+ server.registerTool("verify-site", {
4239
+ description: "Trigger Google to validate a placed verification token.",
4240
+ inputSchema: z.object({
4241
+ siteUrl: z.string(),
4242
+ method: verificationMethodSchema
4243
+ }).shape
4244
+ }, async ({ siteUrl, method }) => {
4245
+ const resource = await verifySite(await getClient(), siteUrl, method);
4246
+ return { content: [{
4247
+ type: "text",
4248
+ text: JSON.stringify({
4249
+ siteUrl,
4250
+ method,
4251
+ resource
4252
+ }, null, 2)
4253
+ }] };
4254
+ });
4255
+ server.registerTool("list-verified-sites", {
4256
+ description: "List verified WebResources from the Site Verification API.",
4257
+ inputSchema: z.object({}).shape
4258
+ }, async () => {
4259
+ const resources = await listVerifiedSites(await getClient());
4260
+ return { content: [{
4261
+ type: "text",
4262
+ text: JSON.stringify(resources, null, 2)
4263
+ }] };
4264
+ });
4265
+ server.registerTool("get-verified-site", {
4266
+ description: "Fetch a single verified WebResource by id.",
4267
+ inputSchema: z.object({ id: z.string().describe("WebResource id (from list-verified-sites)") }).shape
4268
+ }, async ({ id }) => {
4269
+ const resource = await getVerifiedSite(await getClient(), id);
4270
+ return { content: [{
4271
+ type: "text",
4272
+ text: JSON.stringify(resource, null, 2)
4273
+ }] };
4274
+ });
4275
+ server.registerTool("unverify-site", {
4276
+ description: "Drop the calling user's verified ownership of a WebResource. Remove the placed token first or Google may re-verify.",
4277
+ inputSchema: z.object({ id: z.string().describe("WebResource id (from list-verified-sites)") }).shape
4278
+ }, async ({ id }) => {
4279
+ await unverifySite(await getClient(), id);
4280
+ return { content: [{
4281
+ type: "text",
4282
+ text: JSON.stringify({
4283
+ id,
4284
+ status: "unverified"
4285
+ }, null, 2)
4286
+ }] };
4287
+ });
4288
+ server.registerTool("discover-sitemap", {
4289
+ description: "Probe a domain's robots.txt + common paths for an advertised sitemap (no auth).",
4290
+ inputSchema: z.object({ domain: z.string().describe("Domain (e.g., example.com)") }).shape
4291
+ }, async ({ domain }) => {
4292
+ const cleaned = String(domain).replace(/^https?:\/\//, "").replace(/\/.*$/, "");
4293
+ const url = await discoverSitemap(cleaned).catch(() => null);
4294
+ return { content: [{
4295
+ type: "text",
4296
+ text: JSON.stringify({
4297
+ domain: cleaned,
4298
+ sitemap: url
4299
+ }, null, 2)
4300
+ }] };
4301
+ });
4302
+ server.registerTool("batch-get-indexing-status", {
4303
+ description: "Get indexing notification metadata for multiple URLs.",
4304
+ inputSchema: z.object({
4305
+ urls: z.array(z.string()).describe("URLs"),
4306
+ delayMs: z.number().optional().describe("Delay between requests in ms (default 100)"),
4307
+ concurrency: z.number().optional().describe("Concurrent in-flight requests (default 1)")
4308
+ }).shape
4309
+ }, async ({ urls, delayMs, concurrency }) => {
4310
+ const client = await getClient();
4311
+ const results = await runSequentialBatch(urls, (url) => getIndexingMetadata(client, url), {
4312
+ delayMs: delayMs ?? 100,
4313
+ concurrency: concurrency ?? 1
4314
+ });
4315
+ return { content: [{
4316
+ type: "text",
4317
+ text: JSON.stringify(results, null, 2)
4318
+ }] };
4319
+ });
4320
+ return server;
4321
+ }
3630
4322
  async function checkAuth() {
3631
4323
  if (resolveBYOK()) return { ok: true };
3632
4324
  const config = await loadConfig();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/cli",
3
3
  "type": "module",
4
- "version": "0.24.0",
4
+ "version": "0.25.0",
5
5
  "description": "CLI for Google Search Console - dump, query, and run MCP server",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -42,11 +42,11 @@
42
42
  "google-auth-library": "^10.6.2",
43
43
  "ofetch": "^1.5.1",
44
44
  "open": "^11.0.0",
45
- "@gscdump/analysis": "0.24.0",
46
- "@gscdump/engine": "0.24.0",
47
- "@gscdump/engine-gsc-api": "0.24.0",
48
- "@gscdump/mcp": "0.24.0",
49
- "gscdump": "0.24.0"
45
+ "zod": "^4.4.3",
46
+ "@gscdump/analysis": "0.25.0",
47
+ "@gscdump/engine": "0.25.0",
48
+ "@gscdump/engine-gsc-api": "0.25.0",
49
+ "gscdump": "0.25.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@duckdb/node-api": "1.5.1-r.2",