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