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