@gscdump/cli 1.4.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{_chunks/analysis-local.mjs → analysis-local.mjs} +4 -3
- package/dist/{_chunks/auth.mjs → auth.mjs} +2 -2
- package/dist/cli.d.mts +1 -19
- package/dist/cli.mjs +25 -25
- package/dist/{_chunks → commands}/analyze.mjs +3 -3
- package/dist/{_chunks/auth2.mjs → commands/auth.mjs} +4 -4
- package/dist/commands/compact.mjs +128 -0
- package/dist/{_chunks/config2.mjs → commands/config.mjs} +4 -4
- package/dist/{_chunks → commands}/doctor.mjs +8 -7
- package/dist/{_chunks → commands}/dump.mjs +5 -4
- package/dist/{_chunks → commands}/entities.mjs +2 -2
- package/dist/commands/export.mjs +76 -0
- package/dist/commands/gc.mjs +74 -0
- package/dist/{_chunks → commands}/indexing.mjs +3 -3
- package/dist/{_chunks → commands}/init.mjs +5 -5
- package/dist/{_chunks → commands}/inspect.mjs +3 -3
- package/dist/commands/mcp.mjs +57 -0
- package/dist/{_chunks → commands}/profile.mjs +7 -17
- package/dist/{_chunks → commands}/query.mjs +5 -4
- package/dist/{_chunks → commands}/report.mjs +2 -2
- package/dist/commands/rollups.mjs +134 -0
- package/dist/{_chunks → commands}/sitemaps.mjs +3 -3
- package/dist/{_chunks → commands}/sites.mjs +3 -3
- package/dist/commands/stats.mjs +118 -0
- package/dist/commands/store-purge.mjs +93 -0
- package/dist/commands/store.mjs +40 -0
- package/dist/{_chunks → commands}/sync.mjs +3 -2
- package/dist/config.mjs +43 -0
- package/dist/{_chunks/context.mjs → context.mjs} +3 -9
- package/dist/{_chunks/env-file.mjs → env-file.mjs} +3 -3
- package/dist/{_chunks/environment.mjs → environment.mjs} +1 -1
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +2 -0
- package/dist/local-store.mjs +7 -0
- package/dist/mcp/errors.mjs +85 -0
- package/dist/mcp/handlers/analytics.mjs +1 -0
- package/dist/mcp/handlers/diagnostics.mjs +134 -0
- package/dist/mcp/handlers/index.mjs +7 -0
- package/dist/mcp/handlers/indexing.mjs +27 -0
- package/dist/mcp/handlers/query.mjs +5 -0
- package/dist/mcp/handlers/reports.mjs +72 -0
- package/dist/mcp/handlers/sites.mjs +8 -0
- package/dist/mcp/server/index.mjs +417 -0
- package/dist/mcp/types.mjs +86 -0
- package/dist/package.mjs +2 -0
- package/dist/runtime.d.mts +20 -0
- package/dist/runtime.mjs +38 -0
- package/dist/{_chunks/utils.mjs → utils.mjs} +4 -4
- package/package.json +8 -8
- package/dist/_chunks/config.mjs +0 -93
- package/dist/_chunks/mcp.mjs +0 -867
- package/dist/_chunks/store.mjs +0 -628
- /package/dist/{_chunks/error-handler.mjs → error-handler.mjs} +0 -0
- /package/dist/{_chunks/native-duckdb.mjs → native-duckdb.mjs} +0 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { hasGscWriteScope, hasIndexingScope } from "gscdump";
|
|
2
|
+
import { ofetch } from "ofetch";
|
|
3
|
+
const REQUIRED_SCOPES = [
|
|
4
|
+
"https://www.googleapis.com/auth/webmasters",
|
|
5
|
+
"https://www.googleapis.com/auth/indexing",
|
|
6
|
+
"https://www.googleapis.com/auth/siteverification"
|
|
7
|
+
];
|
|
8
|
+
const FETCH_TIMEOUT_MS = 5e3;
|
|
9
|
+
const TIME_SKEW_WARN_MS = 5 * 6e4;
|
|
10
|
+
function hasGoogleScope(scopes, scope) {
|
|
11
|
+
const suffix = scope.replace("https://www.googleapis.com/auth/", "");
|
|
12
|
+
return scopes.includes(scope) || scopes.includes(suffix);
|
|
13
|
+
}
|
|
14
|
+
function missingRequiredScopes(scopes) {
|
|
15
|
+
return REQUIRED_SCOPES.filter((scope) => {
|
|
16
|
+
if (scope.endsWith("/webmasters")) return !hasGscWriteScope(scopes);
|
|
17
|
+
if (scope.endsWith("/indexing")) return !hasIndexingScope(scopes);
|
|
18
|
+
return !hasGoogleScope(scopes, scope);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
async function diagnostics(_input, ctx) {
|
|
22
|
+
const checks = [];
|
|
23
|
+
const token = await resolveAccessToken(ctx.auth);
|
|
24
|
+
if (!token) {
|
|
25
|
+
checks.push({
|
|
26
|
+
name: "auth",
|
|
27
|
+
status: "fail",
|
|
28
|
+
detail: "no usable access token (refresh failed or no credentials)"
|
|
29
|
+
});
|
|
30
|
+
return {
|
|
31
|
+
ok: false,
|
|
32
|
+
checks
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const [tokenInfo, timeCheck, gscReachable, indexingReachable, sites] = await Promise.all([
|
|
36
|
+
ofetch("https://oauth2.googleapis.com/tokeninfo", { query: { access_token: token } }).catch((e) => ({ error: e.message })),
|
|
37
|
+
probeTimeSkew(),
|
|
38
|
+
probeReachable("https://searchconsole.googleapis.com/$discovery/rest?version=v1"),
|
|
39
|
+
probeReachable("https://indexing.googleapis.com/$discovery/rest?version=v3"),
|
|
40
|
+
ctx.client.sites().catch((e) => e)
|
|
41
|
+
]);
|
|
42
|
+
if ("error" in tokenInfo) checks.push({
|
|
43
|
+
name: "auth",
|
|
44
|
+
status: "fail",
|
|
45
|
+
detail: `tokeninfo failed: ${tokenInfo.error}`
|
|
46
|
+
});
|
|
47
|
+
else {
|
|
48
|
+
checks.push({
|
|
49
|
+
name: "auth",
|
|
50
|
+
status: "pass",
|
|
51
|
+
detail: tokenInfo.email ?? "token valid"
|
|
52
|
+
});
|
|
53
|
+
const scopes = tokenInfo.scope ? tokenInfo.scope.split(/\s+/) : [];
|
|
54
|
+
const missing = missingRequiredScopes(scopes);
|
|
55
|
+
checks.push(missing.length > 0 ? {
|
|
56
|
+
name: "auth.scopes",
|
|
57
|
+
status: "warn",
|
|
58
|
+
detail: `missing: ${missing.join(", ")}`
|
|
59
|
+
} : {
|
|
60
|
+
name: "auth.scopes",
|
|
61
|
+
status: "pass",
|
|
62
|
+
detail: `${scopes.length} granted`
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
checks.push(timeCheck);
|
|
66
|
+
checks.push({
|
|
67
|
+
name: "gsc.api",
|
|
68
|
+
status: gscReachable ? "pass" : "warn",
|
|
69
|
+
detail: gscReachable ? "reachable" : "searchconsole.googleapis.com unreachable"
|
|
70
|
+
});
|
|
71
|
+
checks.push({
|
|
72
|
+
name: "indexing.api",
|
|
73
|
+
status: indexingReachable ? "pass" : "warn",
|
|
74
|
+
detail: indexingReachable ? "reachable" : "indexing.googleapis.com unreachable"
|
|
75
|
+
});
|
|
76
|
+
if (sites instanceof Error) checks.push({
|
|
77
|
+
name: "gsc.sites",
|
|
78
|
+
status: "fail",
|
|
79
|
+
detail: `sites() failed: ${sites.message}`
|
|
80
|
+
});
|
|
81
|
+
else {
|
|
82
|
+
const verified = sites.filter((s) => s.permissionLevel !== "siteUnverifiedUser").length;
|
|
83
|
+
checks.push({
|
|
84
|
+
name: "gsc.sites",
|
|
85
|
+
status: "pass",
|
|
86
|
+
detail: `${sites.length} site(s) accessible (${verified} verified)`
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
ok: !checks.some((c) => c.status === "fail"),
|
|
91
|
+
checks
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
async function resolveAccessToken(auth) {
|
|
95
|
+
if (typeof auth === "string") return auth;
|
|
96
|
+
if (auth && typeof auth === "object" && "getAccessToken" in auth) return (await auth.getAccessToken().catch(() => null))?.token ?? null;
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
async function probeTimeSkew() {
|
|
100
|
+
const dateHeader = await ofetch.raw("https://oauth2.googleapis.com/tokeninfo", {
|
|
101
|
+
method: "GET",
|
|
102
|
+
timeout: FETCH_TIMEOUT_MS
|
|
103
|
+
}).then((r) => r.headers.get("date")).catch((e) => e?.response?.headers?.get("date") ?? null);
|
|
104
|
+
if (!dateHeader) return {
|
|
105
|
+
name: "time",
|
|
106
|
+
status: "warn",
|
|
107
|
+
detail: "no Date header"
|
|
108
|
+
};
|
|
109
|
+
const remoteMs = Date.parse(dateHeader);
|
|
110
|
+
if (!Number.isFinite(remoteMs)) return {
|
|
111
|
+
name: "time",
|
|
112
|
+
status: "warn",
|
|
113
|
+
detail: `unparseable Date header: ${dateHeader}`
|
|
114
|
+
};
|
|
115
|
+
const skewMs = Date.now() - remoteMs;
|
|
116
|
+
const human = `${skewMs >= 0 ? "+" : ""}${(skewMs / 1e3).toFixed(1)}s`;
|
|
117
|
+
if (Math.abs(skewMs) > TIME_SKEW_WARN_MS) return {
|
|
118
|
+
name: "time",
|
|
119
|
+
status: "warn",
|
|
120
|
+
detail: `local clock ${human} off Google`
|
|
121
|
+
};
|
|
122
|
+
return {
|
|
123
|
+
name: "time",
|
|
124
|
+
status: "pass",
|
|
125
|
+
detail: `in sync (${human})`
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
async function probeReachable(url) {
|
|
129
|
+
return ofetch.raw(url, {
|
|
130
|
+
method: "GET",
|
|
131
|
+
timeout: FETCH_TIMEOUT_MS
|
|
132
|
+
}).then(() => true).catch(() => false);
|
|
133
|
+
}
|
|
134
|
+
export { diagnostics };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import "./analytics.mjs";
|
|
2
|
+
import { diagnostics } from "./diagnostics.mjs";
|
|
3
|
+
import { batchInspectUrls, batchRequestIndexing, getIndexingStatus, requestIndexing } from "./indexing.mjs";
|
|
4
|
+
import "./query.mjs";
|
|
5
|
+
import { listReports, runReportHandler, runReportHandlerResult } from "./reports.mjs";
|
|
6
|
+
import { getSitemap, listSitesWithSitemaps } from "./sites.mjs";
|
|
7
|
+
export { batchInspectUrls, batchRequestIndexing, diagnostics, getIndexingStatus, getSitemap, listReports, listSitesWithSitemaps, requestIndexing, runReportHandler, runReportHandlerResult };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { batchInspectUrls, batchRequestIndexing, getIndexingMetadata, requestIndexing } from "gscdump";
|
|
2
|
+
async function requestIndexing$1(input, ctx) {
|
|
3
|
+
return requestIndexing(ctx.client, input.url, { type: input.type || "URL_UPDATED" });
|
|
4
|
+
}
|
|
5
|
+
async function getIndexingStatus(input, ctx) {
|
|
6
|
+
return getIndexingMetadata(ctx.client, input.url);
|
|
7
|
+
}
|
|
8
|
+
async function batchRequestIndexing$1(input, ctx) {
|
|
9
|
+
const results = await batchRequestIndexing(ctx.client, input.urls, {
|
|
10
|
+
type: input.type || "URL_UPDATED",
|
|
11
|
+
delayMs: input.delayMs || 100
|
|
12
|
+
});
|
|
13
|
+
return {
|
|
14
|
+
results,
|
|
15
|
+
success: results.length,
|
|
16
|
+
failed: 0
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
async function batchInspectUrls$1(input, ctx) {
|
|
20
|
+
const results = await batchInspectUrls(ctx.client, input.siteUrl, input.urls, { delayMs: input.delayMs || 200 });
|
|
21
|
+
return {
|
|
22
|
+
results,
|
|
23
|
+
indexed: results.filter((r) => r.isIndexed).length,
|
|
24
|
+
notIndexed: results.filter((r) => !r.isIndexed).length
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export { batchInspectUrls$1 as batchInspectUrls, batchRequestIndexing$1 as batchRequestIndexing, getIndexingStatus, requestIndexing$1 as requestIndexing };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { enrichToolError, mcpHandlerErrorToException, mcpHandlerErrors } from "../errors.mjs";
|
|
2
|
+
import { defaultAnalyzerRegistry } from "@gscdump/analysis/registry";
|
|
3
|
+
import { createGscApiQuerySource } from "@gscdump/engine-gsc-api";
|
|
4
|
+
import { err, ok, unwrapResult } from "gscdump/result";
|
|
5
|
+
import { defaultReportRegistry, runReport } from "@gscdump/analysis/report";
|
|
6
|
+
import { resolveWindow } from "@gscdump/engine/period";
|
|
7
|
+
const PERIOD_ALIASES = {
|
|
8
|
+
"7d": "last-7d",
|
|
9
|
+
"28d": "last-28d",
|
|
10
|
+
"30d": "last-30d",
|
|
11
|
+
"90d": "last-90d",
|
|
12
|
+
"180d": "last-180d",
|
|
13
|
+
"365d": "last-365d",
|
|
14
|
+
"mtd": "mtd",
|
|
15
|
+
"ytd": "ytd",
|
|
16
|
+
"custom": "custom"
|
|
17
|
+
};
|
|
18
|
+
const COMPARISON_ALIASES = {
|
|
19
|
+
"none": "none",
|
|
20
|
+
"prev": "prev-period",
|
|
21
|
+
"prev-period": "prev-period",
|
|
22
|
+
"prior": "prev-period",
|
|
23
|
+
"yoy": "yoy"
|
|
24
|
+
};
|
|
25
|
+
function listReports() {
|
|
26
|
+
return defaultReportRegistry.listReports().map((r) => ({
|
|
27
|
+
id: r.id,
|
|
28
|
+
description: r.description,
|
|
29
|
+
defaultPeriod: r.defaultPeriod,
|
|
30
|
+
defaultComparison: r.defaultComparison,
|
|
31
|
+
argsSpec: r.argsSpec
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
async function runReportHandlerResult(input, ctx) {
|
|
35
|
+
const report = defaultReportRegistry.getReport(input.id);
|
|
36
|
+
if (!report) return err(mcpHandlerErrors.unknownReport(input.id, defaultReportRegistry.listReportIds()));
|
|
37
|
+
const preset = input.period ? PERIOD_ALIASES[input.period.toLowerCase()] ?? null : report.defaultPeriod;
|
|
38
|
+
if (!preset) return err(mcpHandlerErrors.unknownPeriod(input.period ?? ""));
|
|
39
|
+
const comparison = input.comparison ? COMPARISON_ALIASES[input.comparison.toLowerCase()] ?? null : report.defaultComparison;
|
|
40
|
+
if (!comparison) return err(mcpHandlerErrors.unknownComparison(input.comparison ?? ""));
|
|
41
|
+
const window = resolveWindow({
|
|
42
|
+
preset,
|
|
43
|
+
comparison,
|
|
44
|
+
start: input.start,
|
|
45
|
+
end: input.end
|
|
46
|
+
});
|
|
47
|
+
if (input.prevStart && input.prevEnd) window.comparison = {
|
|
48
|
+
start: input.prevStart,
|
|
49
|
+
end: input.prevEnd
|
|
50
|
+
};
|
|
51
|
+
const params = {};
|
|
52
|
+
if (input.maxFindings != null) params.maxFindings = input.maxFindings;
|
|
53
|
+
return ok(await runReport(report, {
|
|
54
|
+
source: createGscApiQuerySource({
|
|
55
|
+
client: ctx.client,
|
|
56
|
+
siteUrl: input.siteUrl
|
|
57
|
+
}),
|
|
58
|
+
analyzers: defaultAnalyzerRegistry,
|
|
59
|
+
ctx: {
|
|
60
|
+
site: input.siteUrl,
|
|
61
|
+
window,
|
|
62
|
+
params,
|
|
63
|
+
registryVersion: defaultReportRegistry.version
|
|
64
|
+
}
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
async function runReportHandler(input, ctx) {
|
|
68
|
+
return unwrapResult(await runReportHandlerResult(input, ctx).catch((thrown) => {
|
|
69
|
+
throw enrichToolError(thrown) ?? thrown;
|
|
70
|
+
}), mcpHandlerErrorToException);
|
|
71
|
+
}
|
|
72
|
+
export { listReports, runReportHandler, runReportHandlerResult };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { fetchSitemap, fetchSitesWithSitemaps } from "gscdump";
|
|
2
|
+
async function listSitesWithSitemaps(_input, ctx) {
|
|
3
|
+
return fetchSitesWithSitemaps(ctx.client);
|
|
4
|
+
}
|
|
5
|
+
async function getSitemap(input, ctx) {
|
|
6
|
+
return fetchSitemap(ctx.client, input.siteUrl, input.feedpath);
|
|
7
|
+
}
|
|
8
|
+
export { getSitemap, listSitesWithSitemaps };
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import { diagnostics } from "../handlers/diagnostics.mjs";
|
|
2
|
+
import { batchInspectUrls as batchInspectUrls$1, batchRequestIndexing as batchRequestIndexing$1, getIndexingStatus, requestIndexing as requestIndexing$1 } from "../handlers/indexing.mjs";
|
|
3
|
+
import { listReports, runReportHandler } from "../handlers/reports.mjs";
|
|
4
|
+
import { getSitemap, listSitesWithSitemaps } from "../handlers/sites.mjs";
|
|
5
|
+
import "../handlers/index.mjs";
|
|
6
|
+
import { batchInspectUrlsInput, batchRequestIndexingInput, getIndexingStatusInput, inspectUrlInput, listReportsInput, listSitemapsInput, listSitesInput, requestIndexingInput, runReportInput, sitemapInput } from "../types.mjs";
|
|
7
|
+
import { addSite, deleteSite, discoverSitemap, getIndexingMetadata, getVerificationToken, getVerifiedSite, googleSearchConsole, listVerifiedSites, runSequentialBatch, unverifySite, verifySite } from "gscdump";
|
|
8
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
async function runMcpSearchAnalyticsQuery(client, args) {
|
|
11
|
+
const totalLimit = Math.max(0, args.rowLimit ?? 25e3);
|
|
12
|
+
const pageSize = Math.min(totalLimit, 25e3);
|
|
13
|
+
const allRows = [];
|
|
14
|
+
let startRow = 0;
|
|
15
|
+
while (allRows.length < totalLimit) {
|
|
16
|
+
const remaining = totalLimit - allRows.length;
|
|
17
|
+
const currentLimit = Math.min(pageSize, remaining);
|
|
18
|
+
if (currentLimit <= 0) break;
|
|
19
|
+
const rows = ((await client.searchAnalytics.query(args.siteUrl, {
|
|
20
|
+
startDate: args.startDate,
|
|
21
|
+
endDate: args.endDate,
|
|
22
|
+
dimensions: args.dimensions,
|
|
23
|
+
rowLimit: currentLimit,
|
|
24
|
+
startRow,
|
|
25
|
+
...args.type ? { type: args.type } : {},
|
|
26
|
+
...args.dataState ? { dataState: args.dataState } : {},
|
|
27
|
+
...args.aggregationType ? { aggregationType: args.aggregationType } : {},
|
|
28
|
+
...args.dimensionFilterGroups ? { dimensionFilterGroups: args.dimensionFilterGroups.map((g) => ({
|
|
29
|
+
groupType: g.groupType ?? "and",
|
|
30
|
+
filters: g.filters
|
|
31
|
+
})) } : {}
|
|
32
|
+
})).rows || []).map((row) => {
|
|
33
|
+
const result = {
|
|
34
|
+
clicks: row.clicks ?? 0,
|
|
35
|
+
impressions: row.impressions ?? 0,
|
|
36
|
+
ctr: row.ctr ?? 0,
|
|
37
|
+
position: row.position ?? 0
|
|
38
|
+
};
|
|
39
|
+
args.dimensions.forEach((dim, i) => {
|
|
40
|
+
result[dim] = row.keys?.[i];
|
|
41
|
+
});
|
|
42
|
+
return result;
|
|
43
|
+
});
|
|
44
|
+
if (rows.length === 0) break;
|
|
45
|
+
allRows.push(...rows);
|
|
46
|
+
startRow += rows.length;
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
siteUrl: args.siteUrl,
|
|
50
|
+
rowCount: allRows.length,
|
|
51
|
+
rows: allRows
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function createGscMcpServer(options) {
|
|
55
|
+
const { name = "gscdump", version = "1.0.0", getAuth } = options;
|
|
56
|
+
const server = new McpServer({
|
|
57
|
+
name,
|
|
58
|
+
version
|
|
59
|
+
});
|
|
60
|
+
const auth = async () => Promise.resolve(getAuth());
|
|
61
|
+
const getContext = async () => {
|
|
62
|
+
const a = await auth();
|
|
63
|
+
return {
|
|
64
|
+
auth: a,
|
|
65
|
+
client: googleSearchConsole(a)
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
const getClient = async () => googleSearchConsole(await auth());
|
|
69
|
+
server.registerTool("list-sites", {
|
|
70
|
+
description: "List all Google Search Console sites visible to the authenticated user.",
|
|
71
|
+
inputSchema: listSitesInput.shape
|
|
72
|
+
}, async () => {
|
|
73
|
+
const sites = (await (await getClient()).sites()).filter((s) => s.siteUrl && s.permissionLevel !== "siteUnverifiedUser").map((s) => ({
|
|
74
|
+
siteUrl: s.siteUrl,
|
|
75
|
+
permissionLevel: s.permissionLevel || "unknown"
|
|
76
|
+
}));
|
|
77
|
+
return { content: [{
|
|
78
|
+
type: "text",
|
|
79
|
+
text: JSON.stringify(sites, null, 2)
|
|
80
|
+
}] };
|
|
81
|
+
});
|
|
82
|
+
server.registerTool("list-sites-with-sitemaps", {
|
|
83
|
+
description: "List all GSC sites with their sitemaps",
|
|
84
|
+
inputSchema: listSitesInput.shape
|
|
85
|
+
}, async (args) => {
|
|
86
|
+
const result = await listSitesWithSitemaps(args, await getContext());
|
|
87
|
+
return { content: [{
|
|
88
|
+
type: "text",
|
|
89
|
+
text: JSON.stringify(result, null, 2)
|
|
90
|
+
}] };
|
|
91
|
+
});
|
|
92
|
+
server.registerTool("list-sitemaps", {
|
|
93
|
+
description: "List sitemaps for a specific site",
|
|
94
|
+
inputSchema: listSitemapsInput.shape
|
|
95
|
+
}, async (args) => {
|
|
96
|
+
const sitemaps = await (await getClient()).sitemaps.list(args.siteUrl);
|
|
97
|
+
return { content: [{
|
|
98
|
+
type: "text",
|
|
99
|
+
text: JSON.stringify(sitemaps, null, 2)
|
|
100
|
+
}] };
|
|
101
|
+
});
|
|
102
|
+
server.registerTool("get-sitemap", {
|
|
103
|
+
description: "Get details for a specific sitemap",
|
|
104
|
+
inputSchema: sitemapInput.shape
|
|
105
|
+
}, async (args) => {
|
|
106
|
+
const result = await getSitemap(args, await getContext());
|
|
107
|
+
return { content: [{
|
|
108
|
+
type: "text",
|
|
109
|
+
text: JSON.stringify(result, null, 2)
|
|
110
|
+
}] };
|
|
111
|
+
});
|
|
112
|
+
server.registerTool("submit-sitemap", {
|
|
113
|
+
description: "Submit a sitemap to Google Search Console",
|
|
114
|
+
inputSchema: sitemapInput.shape
|
|
115
|
+
}, async (args) => {
|
|
116
|
+
await (await getClient()).sitemaps.submit(args.siteUrl, args.feedpath);
|
|
117
|
+
return { content: [{
|
|
118
|
+
type: "text",
|
|
119
|
+
text: JSON.stringify({ success: true }, null, 2)
|
|
120
|
+
}] };
|
|
121
|
+
});
|
|
122
|
+
server.registerTool("delete-sitemap", {
|
|
123
|
+
description: "Delete a sitemap from Google Search Console",
|
|
124
|
+
inputSchema: sitemapInput.shape
|
|
125
|
+
}, async (args) => {
|
|
126
|
+
await (await getClient()).sitemaps.delete(args.siteUrl, args.feedpath);
|
|
127
|
+
return { content: [{
|
|
128
|
+
type: "text",
|
|
129
|
+
text: JSON.stringify({ success: true }, null, 2)
|
|
130
|
+
}] };
|
|
131
|
+
});
|
|
132
|
+
server.registerTool("list-reports", {
|
|
133
|
+
description: "List available reports (intent-keyed analyzer compositions). Returns id, description, default period/comparison, and per-report argsSpec.",
|
|
134
|
+
inputSchema: listReportsInput.shape
|
|
135
|
+
}, async () => {
|
|
136
|
+
const result = listReports();
|
|
137
|
+
return { content: [{
|
|
138
|
+
type: "text",
|
|
139
|
+
text: JSON.stringify(result, null, 2)
|
|
140
|
+
}] };
|
|
141
|
+
});
|
|
142
|
+
server.registerTool("run-report", {
|
|
143
|
+
description: "Run a report against the GSC API. Returns a structured ReportResult with bounded findings per section. See list-reports for ids.",
|
|
144
|
+
inputSchema: runReportInput.shape
|
|
145
|
+
}, async (args) => {
|
|
146
|
+
const result = await runReportHandler(args, await getContext());
|
|
147
|
+
return { content: [{
|
|
148
|
+
type: "text",
|
|
149
|
+
text: JSON.stringify(result, null, 2)
|
|
150
|
+
}] };
|
|
151
|
+
});
|
|
152
|
+
const filterOperatorSchema = z.enum([
|
|
153
|
+
"equals",
|
|
154
|
+
"notEquals",
|
|
155
|
+
"contains",
|
|
156
|
+
"notContains",
|
|
157
|
+
"includingRegex",
|
|
158
|
+
"excludingRegex"
|
|
159
|
+
]);
|
|
160
|
+
const dimensionFilterSchema = z.object({
|
|
161
|
+
dimension: z.enum([
|
|
162
|
+
"date",
|
|
163
|
+
"query",
|
|
164
|
+
"page",
|
|
165
|
+
"country",
|
|
166
|
+
"device",
|
|
167
|
+
"searchAppearance"
|
|
168
|
+
]),
|
|
169
|
+
operator: filterOperatorSchema,
|
|
170
|
+
expression: z.string()
|
|
171
|
+
});
|
|
172
|
+
const filterGroupSchema = z.object({
|
|
173
|
+
groupType: z.enum(["and"]).optional().describe("Always \"and\"; multiple groups are OR-ed together"),
|
|
174
|
+
filters: z.array(dimensionFilterSchema)
|
|
175
|
+
});
|
|
176
|
+
server.registerTool("query", {
|
|
177
|
+
description: "Run a custom search analytics query. Supports dimension filters (regex/contains/equals) via dimensionFilterGroups; multiple groups are OR-ed.",
|
|
178
|
+
inputSchema: z.object({
|
|
179
|
+
siteUrl: z.string().describe("GSC property URL (e.g., sc-domain:example.com)"),
|
|
180
|
+
startDate: z.string().describe("Start date (YYYY-MM-DD)"),
|
|
181
|
+
endDate: z.string().describe("End date (YYYY-MM-DD)"),
|
|
182
|
+
dimensions: z.array(z.enum([
|
|
183
|
+
"date",
|
|
184
|
+
"query",
|
|
185
|
+
"page",
|
|
186
|
+
"country",
|
|
187
|
+
"device",
|
|
188
|
+
"searchAppearance"
|
|
189
|
+
])).describe("Dimensions to group by"),
|
|
190
|
+
rowLimit: z.number().optional().describe("Max rows (default 25000)"),
|
|
191
|
+
type: z.enum([
|
|
192
|
+
"web",
|
|
193
|
+
"image",
|
|
194
|
+
"video",
|
|
195
|
+
"news",
|
|
196
|
+
"discover",
|
|
197
|
+
"googleNews"
|
|
198
|
+
]).optional().describe("Search type"),
|
|
199
|
+
dataState: z.enum(["final", "all"]).optional().describe("Data state: final (settled) or all (includes fresh)"),
|
|
200
|
+
aggregationType: z.enum(["byPage", "byProperty"]).optional().describe("Aggregation type"),
|
|
201
|
+
dimensionFilterGroups: z.array(filterGroupSchema).optional().describe("Filter groups (each \"and\"-ed internally; multiple groups are OR-ed)")
|
|
202
|
+
}).shape
|
|
203
|
+
}, async ({ siteUrl, startDate, endDate, dimensions, rowLimit, type, dataState, aggregationType, dimensionFilterGroups }) => {
|
|
204
|
+
const result = await runMcpSearchAnalyticsQuery(await getClient(), {
|
|
205
|
+
siteUrl,
|
|
206
|
+
startDate,
|
|
207
|
+
endDate,
|
|
208
|
+
dimensions,
|
|
209
|
+
rowLimit,
|
|
210
|
+
type,
|
|
211
|
+
dataState,
|
|
212
|
+
aggregationType,
|
|
213
|
+
dimensionFilterGroups
|
|
214
|
+
});
|
|
215
|
+
return { content: [{
|
|
216
|
+
type: "text",
|
|
217
|
+
text: JSON.stringify(result, null, 2)
|
|
218
|
+
}] };
|
|
219
|
+
});
|
|
220
|
+
server.registerTool("inspect-url", {
|
|
221
|
+
description: "Inspect a URL to check its indexing status in Google Search Console",
|
|
222
|
+
inputSchema: inspectUrlInput.shape
|
|
223
|
+
}, async (args) => {
|
|
224
|
+
const result = await (await getClient()).inspect(args.siteUrl, args.inspectionUrl);
|
|
225
|
+
return { content: [{
|
|
226
|
+
type: "text",
|
|
227
|
+
text: JSON.stringify(result, null, 2)
|
|
228
|
+
}] };
|
|
229
|
+
});
|
|
230
|
+
server.registerTool("request-indexing", {
|
|
231
|
+
description: "Request Google to index or remove a URL via the Indexing API",
|
|
232
|
+
inputSchema: requestIndexingInput.shape
|
|
233
|
+
}, async (args) => {
|
|
234
|
+
const result = await requestIndexing$1(args, await getContext());
|
|
235
|
+
return { content: [{
|
|
236
|
+
type: "text",
|
|
237
|
+
text: JSON.stringify(result, null, 2)
|
|
238
|
+
}] };
|
|
239
|
+
});
|
|
240
|
+
server.registerTool("get-indexing-status", {
|
|
241
|
+
description: "Get indexing status metadata for a URL",
|
|
242
|
+
inputSchema: getIndexingStatusInput.shape
|
|
243
|
+
}, async (args) => {
|
|
244
|
+
const result = await getIndexingStatus(args, await getContext());
|
|
245
|
+
return { content: [{
|
|
246
|
+
type: "text",
|
|
247
|
+
text: JSON.stringify(result, null, 2)
|
|
248
|
+
}] };
|
|
249
|
+
});
|
|
250
|
+
server.registerTool("batch-request-indexing", {
|
|
251
|
+
description: "Batch request indexing for multiple URLs with rate limiting",
|
|
252
|
+
inputSchema: batchRequestIndexingInput.shape
|
|
253
|
+
}, async (args) => {
|
|
254
|
+
const result = await batchRequestIndexing$1(args, await getContext());
|
|
255
|
+
return { content: [{
|
|
256
|
+
type: "text",
|
|
257
|
+
text: JSON.stringify(result, null, 2)
|
|
258
|
+
}] };
|
|
259
|
+
});
|
|
260
|
+
server.registerTool("batch-inspect-urls", {
|
|
261
|
+
description: "Batch inspect multiple URLs to check their indexing status",
|
|
262
|
+
inputSchema: batchInspectUrlsInput.shape
|
|
263
|
+
}, async (args) => {
|
|
264
|
+
const result = await batchInspectUrls$1(args, await getContext());
|
|
265
|
+
return { content: [{
|
|
266
|
+
type: "text",
|
|
267
|
+
text: JSON.stringify(result, null, 2)
|
|
268
|
+
}] };
|
|
269
|
+
});
|
|
270
|
+
server.registerTool("diagnostics", {
|
|
271
|
+
description: "Run health checks on the active GSC connection: auth/scopes, time skew, API reachability, sites count.",
|
|
272
|
+
inputSchema: listSitesInput.shape
|
|
273
|
+
}, async (args) => {
|
|
274
|
+
const result = await diagnostics(args, await getContext());
|
|
275
|
+
return { content: [{
|
|
276
|
+
type: "text",
|
|
277
|
+
text: JSON.stringify(result, null, 2)
|
|
278
|
+
}] };
|
|
279
|
+
});
|
|
280
|
+
server.registerTool("add-site", {
|
|
281
|
+
description: "Register a property in Search Console (unverified). Verify ownership separately.",
|
|
282
|
+
inputSchema: z.object({ siteUrl: z.string().describe("Property URL (https://example.com/ or sc-domain:example.com)") }).shape
|
|
283
|
+
}, async ({ siteUrl }) => {
|
|
284
|
+
await addSite(await getClient(), siteUrl);
|
|
285
|
+
return { content: [{
|
|
286
|
+
type: "text",
|
|
287
|
+
text: JSON.stringify({
|
|
288
|
+
siteUrl,
|
|
289
|
+
status: "added",
|
|
290
|
+
verified: false
|
|
291
|
+
}, null, 2)
|
|
292
|
+
}] };
|
|
293
|
+
});
|
|
294
|
+
server.registerTool("delete-site", {
|
|
295
|
+
description: "Remove a property from Search Console.",
|
|
296
|
+
inputSchema: z.object({ siteUrl: z.string().describe("Property URL") }).shape
|
|
297
|
+
}, async ({ siteUrl }) => {
|
|
298
|
+
await deleteSite(await getClient(), siteUrl);
|
|
299
|
+
return { content: [{
|
|
300
|
+
type: "text",
|
|
301
|
+
text: JSON.stringify({
|
|
302
|
+
siteUrl,
|
|
303
|
+
status: "deleted"
|
|
304
|
+
}, null, 2)
|
|
305
|
+
}] };
|
|
306
|
+
});
|
|
307
|
+
const verificationMethodSchema = z.enum([
|
|
308
|
+
"META",
|
|
309
|
+
"FILE",
|
|
310
|
+
"DNS_TXT",
|
|
311
|
+
"DNS_CNAME",
|
|
312
|
+
"ANALYTICS",
|
|
313
|
+
"TAG_MANAGER"
|
|
314
|
+
]);
|
|
315
|
+
server.registerTool("get-verification-token", {
|
|
316
|
+
description: "Get a verification token to place on the site or in DNS.",
|
|
317
|
+
inputSchema: z.object({
|
|
318
|
+
siteUrl: z.string(),
|
|
319
|
+
method: verificationMethodSchema
|
|
320
|
+
}).shape
|
|
321
|
+
}, async ({ siteUrl, method }) => {
|
|
322
|
+
const result = await getVerificationToken(await getClient(), siteUrl, method);
|
|
323
|
+
return { content: [{
|
|
324
|
+
type: "text",
|
|
325
|
+
text: JSON.stringify({
|
|
326
|
+
siteUrl,
|
|
327
|
+
method,
|
|
328
|
+
token: result.token,
|
|
329
|
+
site: result.site
|
|
330
|
+
}, null, 2)
|
|
331
|
+
}] };
|
|
332
|
+
});
|
|
333
|
+
server.registerTool("verify-site", {
|
|
334
|
+
description: "Trigger Google to validate a placed verification token.",
|
|
335
|
+
inputSchema: z.object({
|
|
336
|
+
siteUrl: z.string(),
|
|
337
|
+
method: verificationMethodSchema
|
|
338
|
+
}).shape
|
|
339
|
+
}, async ({ siteUrl, method }) => {
|
|
340
|
+
const resource = await verifySite(await getClient(), siteUrl, method);
|
|
341
|
+
return { content: [{
|
|
342
|
+
type: "text",
|
|
343
|
+
text: JSON.stringify({
|
|
344
|
+
siteUrl,
|
|
345
|
+
method,
|
|
346
|
+
resource
|
|
347
|
+
}, null, 2)
|
|
348
|
+
}] };
|
|
349
|
+
});
|
|
350
|
+
server.registerTool("list-verified-sites", {
|
|
351
|
+
description: "List verified WebResources from the Site Verification API.",
|
|
352
|
+
inputSchema: z.object({}).shape
|
|
353
|
+
}, async () => {
|
|
354
|
+
const resources = await listVerifiedSites(await getClient());
|
|
355
|
+
return { content: [{
|
|
356
|
+
type: "text",
|
|
357
|
+
text: JSON.stringify(resources, null, 2)
|
|
358
|
+
}] };
|
|
359
|
+
});
|
|
360
|
+
server.registerTool("get-verified-site", {
|
|
361
|
+
description: "Fetch a single verified WebResource by id.",
|
|
362
|
+
inputSchema: z.object({ id: z.string().describe("WebResource id (from list-verified-sites)") }).shape
|
|
363
|
+
}, async ({ id }) => {
|
|
364
|
+
const resource = await getVerifiedSite(await getClient(), id);
|
|
365
|
+
return { content: [{
|
|
366
|
+
type: "text",
|
|
367
|
+
text: JSON.stringify(resource, null, 2)
|
|
368
|
+
}] };
|
|
369
|
+
});
|
|
370
|
+
server.registerTool("unverify-site", {
|
|
371
|
+
description: "Drop the calling user's verified ownership of a WebResource. Remove the placed token first or Google may re-verify.",
|
|
372
|
+
inputSchema: z.object({ id: z.string().describe("WebResource id (from list-verified-sites)") }).shape
|
|
373
|
+
}, async ({ id }) => {
|
|
374
|
+
await unverifySite(await getClient(), id);
|
|
375
|
+
return { content: [{
|
|
376
|
+
type: "text",
|
|
377
|
+
text: JSON.stringify({
|
|
378
|
+
id,
|
|
379
|
+
status: "unverified"
|
|
380
|
+
}, null, 2)
|
|
381
|
+
}] };
|
|
382
|
+
});
|
|
383
|
+
server.registerTool("discover-sitemap", {
|
|
384
|
+
description: "Probe a domain's robots.txt + common paths for an advertised sitemap (no auth).",
|
|
385
|
+
inputSchema: z.object({ domain: z.string().describe("Domain (e.g., example.com)") }).shape
|
|
386
|
+
}, async ({ domain }) => {
|
|
387
|
+
const cleaned = String(domain).replace(/^https?:\/\//, "").replace(/\/.*$/, "");
|
|
388
|
+
const url = await discoverSitemap(cleaned).catch(() => null);
|
|
389
|
+
return { content: [{
|
|
390
|
+
type: "text",
|
|
391
|
+
text: JSON.stringify({
|
|
392
|
+
domain: cleaned,
|
|
393
|
+
sitemap: url
|
|
394
|
+
}, null, 2)
|
|
395
|
+
}] };
|
|
396
|
+
});
|
|
397
|
+
server.registerTool("batch-get-indexing-status", {
|
|
398
|
+
description: "Get indexing notification metadata for multiple URLs.",
|
|
399
|
+
inputSchema: z.object({
|
|
400
|
+
urls: z.array(z.string()).describe("URLs"),
|
|
401
|
+
delayMs: z.number().optional().describe("Delay between requests in ms (default 100)"),
|
|
402
|
+
concurrency: z.number().optional().describe("Concurrent in-flight requests (default 1)")
|
|
403
|
+
}).shape
|
|
404
|
+
}, async ({ urls, delayMs, concurrency }) => {
|
|
405
|
+
const client = await getClient();
|
|
406
|
+
const results = await runSequentialBatch(urls, (url) => getIndexingMetadata(client, url), {
|
|
407
|
+
delayMs: delayMs ?? 100,
|
|
408
|
+
concurrency: concurrency ?? 1
|
|
409
|
+
});
|
|
410
|
+
return { content: [{
|
|
411
|
+
type: "text",
|
|
412
|
+
text: JSON.stringify(results, null, 2)
|
|
413
|
+
}] };
|
|
414
|
+
});
|
|
415
|
+
return server;
|
|
416
|
+
}
|
|
417
|
+
export { McpServer, createGscMcpServer, runMcpSearchAnalyticsQuery };
|