mcp-google-gsc 1.1.2 → 1.2.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 +57 -15
- package/config.example.json +3 -1
- package/dist/auth-cli.d.ts +10 -0
- package/dist/auth-cli.js +54 -15
- package/dist/authMode.d.ts +22 -0
- package/dist/authMode.js +14 -0
- package/dist/build-info.json +2 -2
- package/dist/credentials.js +4 -4
- package/dist/index.d.ts +52 -0
- package/dist/index.js +61 -14
- package/dist/oauthScope.d.ts +18 -0
- package/dist/oauthScope.js +25 -0
- package/dist/pkce.d.ts +9 -0
- package/dist/pkce.js +24 -0
- package/dist/resilience.js +41 -23
- package/get-refresh-token.cjs +296 -0
- package/package.json +18 -6
- package/dist/auth-cli.js.map +0 -7
- package/dist/credentials.js.map +0 -7
- package/dist/embedded-secrets.js.map +0 -7
- package/dist/errors.js.map +0 -7
- package/dist/index.js.map +0 -7
- package/dist/platform.js.map +0 -7
- package/dist/resilience.js.map +0 -7
- package/dist/tools.js.map +0 -7
- package/dist/updateNotifier.d.ts +0 -7
- package/dist/updateNotifier.js +0 -53
- package/dist/updateNotifier.js.map +0 -7
package/dist/resilience.js
CHANGED
|
@@ -29,6 +29,29 @@ const logger = pino(
|
|
|
29
29
|
pino.destination(2)
|
|
30
30
|
);
|
|
31
31
|
const MAX_RESPONSE_SIZE = 2e5;
|
|
32
|
+
function truncateArraysRecursively(obj, depth = 0) {
|
|
33
|
+
if (depth > 30) return false;
|
|
34
|
+
let truncated = false;
|
|
35
|
+
if (Array.isArray(obj)) {
|
|
36
|
+
if (obj.length > 1) {
|
|
37
|
+
const newLength = Math.max(1, Math.floor(obj.length * 0.5));
|
|
38
|
+
obj.splice(newLength);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
} else if (typeof obj === "object" && obj !== null) {
|
|
42
|
+
for (const key in obj) {
|
|
43
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
44
|
+
if (truncateArraysRecursively(obj[key], depth + 1)) {
|
|
45
|
+
truncated = true;
|
|
46
|
+
if (key === "rows" && Array.isArray(obj[key]) && "row_count" in obj) {
|
|
47
|
+
obj.row_count = obj[key].length;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return truncated;
|
|
54
|
+
}
|
|
32
55
|
function safeResponse(data, context) {
|
|
33
56
|
let current = data;
|
|
34
57
|
for (let pass = 0; pass < 10; pass++) {
|
|
@@ -39,24 +62,11 @@ function safeResponse(data, context) {
|
|
|
39
62
|
current = JSON.parse(JSON.stringify(current));
|
|
40
63
|
}
|
|
41
64
|
logger.warn({ sizeBytes, maxSize: MAX_RESPONSE_SIZE, context, pass }, "Response exceeds size limit, truncating");
|
|
42
|
-
if (
|
|
43
|
-
current
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
if (typeof current === "object" && current !== null) {
|
|
47
|
-
const obj = current;
|
|
48
|
-
let truncated = false;
|
|
49
|
-
for (const key of ["items", "results", "data", "rows", "tags", "triggers", "variables"]) {
|
|
50
|
-
if (Array.isArray(obj[key]) && obj[key].length > 1) {
|
|
51
|
-
obj[key] = obj[key].slice(0, Math.max(1, Math.floor(obj[key].length * 0.5)));
|
|
52
|
-
if ("count" in obj) obj.count = obj[key].length;
|
|
53
|
-
if ("row_count" in obj) obj.row_count = obj[key].length;
|
|
54
|
-
obj.truncated = true;
|
|
55
|
-
truncated = true;
|
|
56
|
-
break;
|
|
57
|
-
}
|
|
65
|
+
if (truncateArraysRecursively(current)) {
|
|
66
|
+
if (typeof current === "object" && current !== null && !Array.isArray(current)) {
|
|
67
|
+
current.truncated = true;
|
|
58
68
|
}
|
|
59
|
-
|
|
69
|
+
continue;
|
|
60
70
|
}
|
|
61
71
|
break;
|
|
62
72
|
}
|
|
@@ -79,15 +89,23 @@ const retryPolicy = retry(isTransient, {
|
|
|
79
89
|
maxAttempts: 3,
|
|
80
90
|
backoff
|
|
81
91
|
});
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
92
|
+
const timeoutPolicy = timeout(3e4, TimeoutStrategy.Aggressive);
|
|
93
|
+
const policyCache = /* @__PURE__ */ new Map();
|
|
94
|
+
function getPolicyForOperation(operationName) {
|
|
95
|
+
if (!policyCache.has(operationName)) {
|
|
96
|
+
const breaker = circuitBreaker(isTransient, {
|
|
97
|
+
halfOpenAfter: 6e4,
|
|
98
|
+
breaker: new ConsecutiveBreaker(5)
|
|
99
|
+
});
|
|
100
|
+
const policy = wrap(timeoutPolicy, breaker, retryPolicy);
|
|
101
|
+
policyCache.set(operationName, policy);
|
|
102
|
+
}
|
|
103
|
+
return policyCache.get(operationName);
|
|
104
|
+
}
|
|
88
105
|
async function withResilience(fn, operationName) {
|
|
89
106
|
try {
|
|
90
107
|
logger.debug({ operation: operationName }, "Starting API call");
|
|
108
|
+
const policy = getPolicyForOperation(operationName);
|
|
91
109
|
const result = await policy.execute(() => fn());
|
|
92
110
|
logger.debug({ operation: operationName }, "API call succeeded");
|
|
93
111
|
return result;
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Standalone OAuth2 refresh-token helper for mcp-gsc (Google Search Console).
|
|
4
|
+
*
|
|
5
|
+
* Publishable: reads everything from env + this repo's config.json. It never
|
|
6
|
+
* reads any file under a user's home directory and never touches a shared
|
|
7
|
+
* OAuth-client keyfile. A user brings THEIR OWN Google OAuth client
|
|
8
|
+
* (client_id + client_secret) created in their own GCP project.
|
|
9
|
+
*
|
|
10
|
+
* Security hardening: uses PKCE (RFC 7636, S256) on top of Google's
|
|
11
|
+
* installed-app loopback flow.
|
|
12
|
+
*
|
|
13
|
+
* === Prerequisites ===
|
|
14
|
+
* 1. Google Cloud Console → APIs & Services → Credentials → create an
|
|
15
|
+
* OAuth 2.0 Client ID of type "Desktop app". Enable the Search Console API.
|
|
16
|
+
* 2. export GOOGLE_GSC_CLIENT_ID=... GOOGLE_GSC_CLIENT_SECRET=...
|
|
17
|
+
* 3. node get-refresh-token.cjs
|
|
18
|
+
* 4. Approve in the browser (as the Google account that owns the GSC data).
|
|
19
|
+
* 5. Copy the printed GOOGLE_GSC_REFRESH_TOKEN=... into your environment.
|
|
20
|
+
*
|
|
21
|
+
* The OAuth scope requested is read from this repo's config.json (oauth.scope),
|
|
22
|
+
* falling back to the read-only webmasters default — the SAME source the MCP
|
|
23
|
+
* runtime uses (src/oauthScope.ts), so helper and runtime never drift.
|
|
24
|
+
*
|
|
25
|
+
* NOTE: this is the USER-OAUTH onboarding path. mcp-gsc also supports a
|
|
26
|
+
* SERVICE-ACCOUNT path (set GOOGLE_APPLICATION_CREDENTIALS to a JSON key file);
|
|
27
|
+
* that path uses no refresh token and does not run this helper.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
"use strict";
|
|
31
|
+
|
|
32
|
+
const http = require("http");
|
|
33
|
+
const crypto = require("crypto");
|
|
34
|
+
const { readFileSync, existsSync } = require("fs");
|
|
35
|
+
const { join } = require("path");
|
|
36
|
+
|
|
37
|
+
const AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
38
|
+
const TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
39
|
+
const DEFAULT_GSC_SCOPE = "https://www.googleapis.com/auth/webmasters.readonly";
|
|
40
|
+
|
|
41
|
+
// Canonical loopback redirect form shared by BOTH onboarding paths (this helper
|
|
42
|
+
// and src/auth-cli.ts). Google matches loopback redirect URIs on scheme + host +
|
|
43
|
+
// path and IGNORES the port, so onboarders register ONE pattern:
|
|
44
|
+
// http://localhost/callback
|
|
45
|
+
// Use host `localhost` (not 127.0.0.1) and path `/callback` everywhere.
|
|
46
|
+
const LOOPBACK_HOST = "localhost";
|
|
47
|
+
const LOOPBACK_PATH = "/callback";
|
|
48
|
+
function buildLoopbackRedirectUri(port) {
|
|
49
|
+
return `http://${LOOPBACK_HOST}:${port}${LOOPBACK_PATH}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── PKCE (RFC 7636) ─────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
function base64url(buf) {
|
|
55
|
+
return buf
|
|
56
|
+
.toString("base64")
|
|
57
|
+
.replace(/\+/g, "-")
|
|
58
|
+
.replace(/\//g, "_")
|
|
59
|
+
.replace(/=+$/, "");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 43–128 chars of the unreserved set (RFC 7636 §4.1). 32 random bytes → 43 chars base64url. */
|
|
63
|
+
function generateCodeVerifier() {
|
|
64
|
+
return base64url(crypto.randomBytes(32));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** code_challenge = base64url(SHA256(code_verifier)), no padding (S256). */
|
|
68
|
+
function computeCodeChallenge(verifier) {
|
|
69
|
+
return base64url(crypto.createHash("sha256").update(verifier, "ascii").digest());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── Scope resolution (shared contract with src/oauthScope.ts) ────────────────
|
|
73
|
+
|
|
74
|
+
function normalizeScope(raw) {
|
|
75
|
+
return String(raw || "")
|
|
76
|
+
.split(/[\s,]+/)
|
|
77
|
+
.map((s) => s.trim())
|
|
78
|
+
.filter(Boolean)
|
|
79
|
+
.join(" ");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Resolve oauth.scope from a parsed config object; falls back to the readonly default. */
|
|
83
|
+
function resolveScopeFromConfig(config) {
|
|
84
|
+
const scope =
|
|
85
|
+
config && typeof config === "object" && config.oauth && typeof config.oauth.scope === "string"
|
|
86
|
+
? config.oauth.scope
|
|
87
|
+
: "";
|
|
88
|
+
return normalizeScope(scope) || DEFAULT_GSC_SCOPE;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Read oauth.scope from a config file on disk; falls back to the readonly default. */
|
|
92
|
+
function loadScopeFromConfigFile(filePath) {
|
|
93
|
+
if (!existsSync(filePath)) return DEFAULT_GSC_SCOPE;
|
|
94
|
+
try {
|
|
95
|
+
return resolveScopeFromConfig(JSON.parse(readFileSync(filePath, "utf-8")));
|
|
96
|
+
} catch {
|
|
97
|
+
return DEFAULT_GSC_SCOPE;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Resolve the scope from a directory's config.json, else the committed default.
|
|
103
|
+
*
|
|
104
|
+
* This is BYTE-IDENTICAL to the runtime (src/oauthScope.ts loadOAuthScopeFromFile
|
|
105
|
+
* on <dir>/config.json): config.json present -> its oauth.scope; absent -> the
|
|
106
|
+
* default. It deliberately does NOT read config.example.json — that committed,
|
|
107
|
+
* editable file must never influence the scope, or the helper could mint a token
|
|
108
|
+
* against a scope the runtime never requests.
|
|
109
|
+
*/
|
|
110
|
+
function resolveScopeFromDir(dir) {
|
|
111
|
+
return loadScopeFromConfigFile(join(dir, "config.json"));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Resolve the scope for THIS install (helper's own directory). */
|
|
115
|
+
function loadScope() {
|
|
116
|
+
return resolveScopeFromDir(__dirname);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── Env validation ───────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
/** Require both client creds from an env-like object; throw a clear error otherwise. */
|
|
122
|
+
function requireClientCreds(env) {
|
|
123
|
+
const clientId = (env.GOOGLE_GSC_CLIENT_ID || "").trim();
|
|
124
|
+
const clientSecret = (env.GOOGLE_GSC_CLIENT_SECRET || "").trim();
|
|
125
|
+
const missing = [];
|
|
126
|
+
if (!clientId) missing.push("GOOGLE_GSC_CLIENT_ID");
|
|
127
|
+
if (!clientSecret) missing.push("GOOGLE_GSC_CLIENT_SECRET");
|
|
128
|
+
if (missing.length) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`Missing required env var(s): ${missing.join(", ")}.\n` +
|
|
131
|
+
`Create a "Desktop app" OAuth client at https://console.cloud.google.com/apis/credentials, ` +
|
|
132
|
+
`then export GOOGLE_GSC_CLIENT_ID and GOOGLE_GSC_CLIENT_SECRET.`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
return { clientId, clientSecret };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── URL / param builders (pure) ──────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
function buildAuthUrl({ clientId, redirectUri, scope, state, codeChallenge }) {
|
|
141
|
+
const params = new URLSearchParams({
|
|
142
|
+
client_id: clientId,
|
|
143
|
+
redirect_uri: redirectUri,
|
|
144
|
+
response_type: "code",
|
|
145
|
+
scope,
|
|
146
|
+
access_type: "offline", // REQUIRED for Google to return a refresh_token
|
|
147
|
+
prompt: "consent", // force a refresh_token even on re-consent
|
|
148
|
+
state,
|
|
149
|
+
code_challenge: codeChallenge,
|
|
150
|
+
code_challenge_method: "S256",
|
|
151
|
+
});
|
|
152
|
+
return `${AUTH_URL}?${params.toString()}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function buildTokenExchangeParams({ code, clientId, clientSecret, redirectUri, codeVerifier }) {
|
|
156
|
+
return new URLSearchParams({
|
|
157
|
+
grant_type: "authorization_code",
|
|
158
|
+
code,
|
|
159
|
+
client_id: clientId,
|
|
160
|
+
client_secret: clientSecret,
|
|
161
|
+
redirect_uri: redirectUri,
|
|
162
|
+
code_verifier: codeVerifier, // PKCE proof — sent on exchange
|
|
163
|
+
}).toString();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Live flow (guarded behind main; not exercised by unit tests) ─────────────
|
|
167
|
+
|
|
168
|
+
async function run() {
|
|
169
|
+
const { clientId, clientSecret } = requireClientCreds(process.env);
|
|
170
|
+
const scope = loadScope();
|
|
171
|
+
const port = Number(process.env.OAUTH_CALLBACK_PORT || 8123);
|
|
172
|
+
const redirectUri = buildLoopbackRedirectUri(port);
|
|
173
|
+
|
|
174
|
+
const state = crypto.randomBytes(16).toString("hex");
|
|
175
|
+
const codeVerifier = generateCodeVerifier();
|
|
176
|
+
const codeChallenge = computeCodeChallenge(codeVerifier);
|
|
177
|
+
const authUrl = buildAuthUrl({ clientId, redirectUri, scope, state, codeChallenge });
|
|
178
|
+
|
|
179
|
+
const code = await new Promise((resolve, reject) => {
|
|
180
|
+
let settled = false;
|
|
181
|
+
const done = (fn) => {
|
|
182
|
+
if (settled) return;
|
|
183
|
+
settled = true;
|
|
184
|
+
fn();
|
|
185
|
+
};
|
|
186
|
+
const server = http.createServer((req, res) => {
|
|
187
|
+
if (!req.url || !req.url.startsWith("/callback")) {
|
|
188
|
+
res.writeHead(404).end();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const url = new URL(req.url, redirectUri);
|
|
192
|
+
const err = url.searchParams.get("error");
|
|
193
|
+
const returnedCode = url.searchParams.get("code");
|
|
194
|
+
const returnedState = url.searchParams.get("state");
|
|
195
|
+
if (err) {
|
|
196
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
197
|
+
res.end("<h1>Authorization denied</h1><p>You can close this tab.</p>");
|
|
198
|
+
done(() => {
|
|
199
|
+
server.close();
|
|
200
|
+
reject(new Error(`OAuth denied: ${err}`));
|
|
201
|
+
});
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (returnedState !== state) {
|
|
205
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
206
|
+
res.end("<h1>State mismatch</h1><p>Possible CSRF. Re-run the command.</p>");
|
|
207
|
+
done(() => {
|
|
208
|
+
server.close();
|
|
209
|
+
reject(new Error("OAuth state mismatch -- possible CSRF"));
|
|
210
|
+
});
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (!returnedCode) {
|
|
214
|
+
res.writeHead(204).end();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
218
|
+
res.end("<h1>Signed in</h1><p>You can close this tab and return to the terminal.</p>");
|
|
219
|
+
done(() => {
|
|
220
|
+
setTimeout(() => server.close(), 200);
|
|
221
|
+
resolve(returnedCode);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
server.on("error", (e) => done(() => reject(new Error(`Loopback server failed: ${e.message}`))));
|
|
225
|
+
server.listen(port, "127.0.0.1", async () => {
|
|
226
|
+
process.stderr.write(`\nCallback server listening on ${redirectUri}\n`);
|
|
227
|
+
process.stderr.write("Opening your browser to sign in with Google...\n");
|
|
228
|
+
process.stderr.write(`If it doesn't open, visit:\n ${authUrl}\n\n`);
|
|
229
|
+
try {
|
|
230
|
+
const openMod = await import("open");
|
|
231
|
+
await (openMod.default || openMod)(authUrl);
|
|
232
|
+
} catch {
|
|
233
|
+
process.stderr.write("Could not open a browser automatically; paste the URL above.\n");
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
setTimeout(
|
|
237
|
+
() =>
|
|
238
|
+
done(() => {
|
|
239
|
+
server.close();
|
|
240
|
+
reject(new Error("Timed out waiting for OAuth callback (5 minutes)."));
|
|
241
|
+
}),
|
|
242
|
+
5 * 60 * 1000,
|
|
243
|
+
);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
process.stderr.write("Authorization code received. Exchanging for tokens...\n");
|
|
247
|
+
const resp = await fetch(TOKEN_URL, {
|
|
248
|
+
method: "POST",
|
|
249
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
250
|
+
body: buildTokenExchangeParams({ code, clientId, clientSecret, redirectUri, codeVerifier }),
|
|
251
|
+
});
|
|
252
|
+
const data = await resp.json();
|
|
253
|
+
|
|
254
|
+
if (!data.refresh_token) {
|
|
255
|
+
process.stderr.write(
|
|
256
|
+
"No refresh_token returned. If you previously granted consent, revoke it at " +
|
|
257
|
+
"https://myaccount.google.com/permissions and re-run.\n",
|
|
258
|
+
);
|
|
259
|
+
// Do not print the raw token response (may contain an access_token).
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// The refresh token is the intended output. It goes to STDOUT so it can be
|
|
264
|
+
// captured; nothing else sensitive is printed. Do NOT run this with stdout
|
|
265
|
+
// redirected to a shared log.
|
|
266
|
+
process.stdout.write(`GOOGLE_GSC_REFRESH_TOKEN=${data.refresh_token}\n`);
|
|
267
|
+
process.stderr.write("\nDone. Set the line above in your environment.\n");
|
|
268
|
+
process.exit(0);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
module.exports = {
|
|
272
|
+
base64url,
|
|
273
|
+
generateCodeVerifier,
|
|
274
|
+
computeCodeChallenge,
|
|
275
|
+
normalizeScope,
|
|
276
|
+
resolveScopeFromConfig,
|
|
277
|
+
loadScopeFromConfigFile,
|
|
278
|
+
resolveScopeFromDir,
|
|
279
|
+
loadScope,
|
|
280
|
+
requireClientCreds,
|
|
281
|
+
buildAuthUrl,
|
|
282
|
+
buildTokenExchangeParams,
|
|
283
|
+
buildLoopbackRedirectUri,
|
|
284
|
+
LOOPBACK_HOST,
|
|
285
|
+
LOOPBACK_PATH,
|
|
286
|
+
AUTH_URL,
|
|
287
|
+
TOKEN_URL,
|
|
288
|
+
DEFAULT_GSC_SCOPE,
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
if (require.main === module) {
|
|
292
|
+
run().catch((err) => {
|
|
293
|
+
process.stderr.write(`\nError: ${err.message}\n`);
|
|
294
|
+
process.exit(1);
|
|
295
|
+
});
|
|
296
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-google-gsc",
|
|
3
3
|
"mcpName": "io.github.mharnett/google-gsc",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.2.0",
|
|
5
5
|
"description": "MCP server for Google Search Console API with multi-client support, search analytics, and URL inspection.",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
@@ -17,10 +17,14 @@
|
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
|
-
"dist",
|
|
20
|
+
"dist/**/*.js",
|
|
21
|
+
"dist/**/*.d.ts",
|
|
22
|
+
"dist/build-info.json",
|
|
23
|
+
"!dist/**/*.test.*",
|
|
21
24
|
"README.md",
|
|
22
25
|
"LICENSE",
|
|
23
|
-
"config.example.json"
|
|
26
|
+
"config.example.json",
|
|
27
|
+
"get-refresh-token.cjs"
|
|
24
28
|
],
|
|
25
29
|
"scripts": {
|
|
26
30
|
"build": "node scripts/build.mjs",
|
|
@@ -28,7 +32,9 @@
|
|
|
28
32
|
"dev": "tsx src/index.ts",
|
|
29
33
|
"dev:auth": "tsx src/auth-cli.ts",
|
|
30
34
|
"auth": "node dist/auth-cli.js",
|
|
31
|
-
"
|
|
35
|
+
"pretest": "npm run build",
|
|
36
|
+
"test": "vitest run",
|
|
37
|
+
"smoke": "scripts/healthcheck.sh"
|
|
32
38
|
},
|
|
33
39
|
"author": "drak-marketing",
|
|
34
40
|
"license": "MIT",
|
|
@@ -40,18 +46,24 @@
|
|
|
40
46
|
"cockatiel": "^3.2.1",
|
|
41
47
|
"env-paths": "^3.0.0",
|
|
42
48
|
"googleapis": "^144.0.0",
|
|
49
|
+
"mcp-updatenotifier": "^1.0.0",
|
|
43
50
|
"open": "^10.0.0",
|
|
44
51
|
"pino": "^8.21.0",
|
|
45
52
|
"pino-pretty": "^13.1.3",
|
|
46
|
-
"prompts": "^2.4.2"
|
|
53
|
+
"prompts": "^2.4.2",
|
|
54
|
+
"googleapis-common": "^7.2.0"
|
|
47
55
|
},
|
|
48
56
|
"devDependencies": {
|
|
49
|
-
"@drak/mcp-test-harness": "
|
|
57
|
+
"@drak-marketing/mcp-test-harness": "^0.1.2",
|
|
50
58
|
"@types/node": "^20.10.0",
|
|
51
59
|
"@types/prompts": "^2.4.9",
|
|
52
60
|
"esbuild": "^0.25.0",
|
|
53
61
|
"tsx": "^4.7.0",
|
|
54
62
|
"typescript": "^5.3.0",
|
|
55
63
|
"vitest": "^4.0.18"
|
|
64
|
+
},
|
|
65
|
+
"repository": {
|
|
66
|
+
"type": "git",
|
|
67
|
+
"url": "git+https://github.com/mharnett/mcp-search-console.git"
|
|
56
68
|
}
|
|
57
69
|
}
|
package/dist/auth-cli.js.map
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../src/auth-cli.ts"],
|
|
4
|
-
"sourcesContent": ["#!/usr/bin/env node\n// ============================================\n// mcp-gsc-auth -- one-time OAuth + site selection\n// ============================================\n// Flow:\n// 1. Loopback HTTP listener on a free port\n// 2. Open browser to Google OAuth consent screen (webmasters.readonly scope)\n// 3. Exchange code for tokens\n// 4. Enumerate accessible GSC sites\n// 5. User picks which site to use as default\n// 6. Write credentials to ~/.config/mcp-gsc-nodejs/credentials.json\n\nimport { google } from \"googleapis\";\nimport http from \"http\";\nimport promptsImport from \"prompts\";\nimport { URL } from \"url\";\nimport { writeStoredCredentials, credentialsFilePath, CREDENTIALS_FILE_VERSION, type StoredCredentials } from \"./credentials.js\";\nimport { EMBEDDED_CLIENT_ID, EMBEDDED_CLIENT_SECRET } from \"./embedded-secrets.js\";\nimport { classifyError, GscAuthError } from \"./errors.js\";\nimport { findFreeLoopbackPort, openBrowser } from \"./platform.js\";\nimport { logger, withResilience } from \"./resilience.js\";\n\nconst prompts = (promptsImport as unknown as { default?: typeof promptsImport }).default ?? promptsImport;\n\nconst OAUTH_SCOPE = \"https://www.googleapis.com/auth/webmasters.readonly\";\nconst OAUTH_AUTH_URL = \"https://accounts.google.com/o/oauth2/v2/auth\";\nconst OAUTH_TOKEN_URL = \"https://oauth2.googleapis.com/token\";\n\ninterface CliArgs {\n siteUrl?: string;\n help: boolean;\n}\n\nfunction parseArgs(argv: string[]): CliArgs {\n const args: CliArgs = { help: false };\n for (let i = 0; i < argv.length; i++) {\n const a = argv[i];\n if (a === \"--help\" || a === \"-h\") args.help = true;\n else if (a === \"--site-url\" && argv[i + 1]) {\n args.siteUrl = argv[++i];\n }\n }\n return args;\n}\n\nfunction printHelp(): void {\n process.stdout.write(\n [\n \"mcp-gsc-auth -- authorize Claude to access your Google Search Console data\",\n \"\",\n \"Usage:\",\n \" npx mcp-gsc-auth\",\n \" npx mcp-gsc-auth --site-url https://example.com/\",\n \"\",\n \"Options:\",\n \" --site-url <url> Skip the site picker and use this property directly\",\n \" -h, --help Show this help\",\n \"\",\n `Credentials are written to: ${credentialsFilePath}`,\n \"\",\n ].join(\"\\n\"),\n );\n}\n\n// ============================================\n// OAUTH: LOOPBACK REDIRECT FLOW\n// ============================================\n\nfunction buildAuthUrl(clientId: string, redirectUri: string, state: string): string {\n const params = new URLSearchParams({\n client_id: clientId,\n redirect_uri: redirectUri,\n response_type: \"code\",\n scope: OAUTH_SCOPE,\n access_type: \"offline\",\n prompt: \"consent\",\n state,\n });\n return `${OAUTH_AUTH_URL}?${params.toString()}`;\n}\n\ninterface AuthorizationCode {\n code: string;\n state: string;\n}\n\nasync function waitForAuthorizationCode(\n port: number,\n expectedState: string,\n authUrl: string,\n): Promise<AuthorizationCode> {\n return new Promise<AuthorizationCode>((resolve, reject) => {\n let settled = false;\n const finish = (fn: () => void) => {\n if (settled) return;\n settled = true;\n fn();\n };\n\n const server = http.createServer((req, res) => {\n if (!req.url) {\n res.writeHead(404).end();\n return;\n }\n const parsed = new URL(req.url, `http://127.0.0.1:${port}`);\n const code = parsed.searchParams.get(\"code\");\n const state = parsed.searchParams.get(\"state\");\n const error = parsed.searchParams.get(\"error\");\n\n if (error) {\n res.writeHead(400, { \"Content-Type\": \"text/html; charset=utf-8\" });\n res.end(renderPage(\"Authorization was denied\", `Google returned: ${escapeHtml(error)}. Close this tab and re-run the command.`));\n finish(() => { server.close(); reject(new GscAuthError(`OAuth denied: ${error}`)); });\n return;\n }\n\n if (!code) {\n res.writeHead(204).end();\n return;\n }\n\n if (state !== expectedState) {\n res.writeHead(400, { \"Content-Type\": \"text/html; charset=utf-8\" });\n res.end(renderPage(\"Security check failed\", \"The state parameter did not match. Please re-run the command.\"));\n finish(() => { server.close(); reject(new GscAuthError(\"OAuth state mismatch\")); });\n return;\n }\n\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" });\n res.end(renderPage(\"Signed in successfully\", \"You can close this tab and return to the terminal.\"));\n finish(() => {\n setTimeout(() => server.close(), 200);\n resolve({ code, state });\n });\n });\n\n server.on(\"error\", (err) => {\n finish(() => reject(new Error(`Loopback server failed: ${err.message}`)));\n });\n\n server.listen(port, \"127.0.0.1\", () => {\n process.stderr.write(`\\nOpening your browser to sign in with Google...\\n`);\n process.stderr.write(`If it doesn't open automatically, visit:\\n ${authUrl}\\n\\n`);\n openBrowser(authUrl).catch((err) => {\n logger.warn({ err: err.message }, \"openBrowser failed\");\n });\n });\n\n setTimeout(() => {\n finish(() => { server.close(); reject(new Error(\"Timed out waiting for OAuth callback (5 minutes).\")); });\n }, 5 * 60 * 1000);\n });\n}\n\nfunction renderPage(title: string, body: string): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\"><title>${escapeHtml(title)}</title>\n <style>body{font:15px -apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,sans-serif;max-width:480px;margin:80px auto;padding:0 24px;color:#222}h1{font-size:22px;margin-bottom:12px}p{line-height:1.5}</style>\n</head>\n<body><h1>${escapeHtml(title)}</h1><p>${escapeHtml(body)}</p></body>\n</html>`;\n}\n\nfunction escapeHtml(s: string): string {\n return s.replace(/[&<>\"']/g, (c) => ({ \"&\": \"&\", \"<\": \"<\", \">\": \">\", '\"': \""\", \"'\": \"'\" })[c]!);\n}\n\n// ============================================\n// TOKEN EXCHANGE\n// ============================================\n\ninterface TokenResponse {\n access_token: string;\n refresh_token: string;\n expires_in: number;\n scope: string;\n token_type: string;\n}\n\nasync function exchangeCodeForTokens(\n code: string,\n clientId: string,\n clientSecret: string,\n redirectUri: string,\n): Promise<TokenResponse> {\n return withResilience(async () => {\n const body = new URLSearchParams({\n code,\n client_id: clientId,\n client_secret: clientSecret,\n redirect_uri: redirectUri,\n grant_type: \"authorization_code\",\n });\n const res = await fetch(OAUTH_TOKEN_URL, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: body.toString(),\n });\n const json = (await res.json()) as Record<string, unknown>;\n if (!res.ok || json.error) {\n const err = new Error(\n `Token exchange failed: ${json.error_description || json.error || res.statusText}`,\n );\n (err as any).status = res.status;\n (err as any).code = res.status;\n throw err;\n }\n return json as unknown as TokenResponse;\n }, \"oauth.exchangeCode\");\n}\n\n// ============================================\n// SITE ENUMERATION\n// ============================================\n\ninterface GscSite {\n siteUrl: string;\n permissionLevel: string;\n}\n\nasync function enumerateSites(accessToken: string): Promise<GscSite[]> {\n const oauth2Client = new google.auth.OAuth2();\n oauth2Client.setCredentials({ access_token: accessToken });\n\n const svc = google.searchconsole({ version: \"v1\", auth: oauth2Client });\n const resp = await withResilience(\n () => svc.sites.list(),\n \"auth.listSites\",\n );\n\n const sites = (resp.data.siteEntry || []).map((entry) => ({\n siteUrl: entry.siteUrl || \"\",\n permissionLevel: entry.permissionLevel || \"\",\n }));\n\n if (sites.length === 0) {\n throw new GscAuthError(\n \"No Search Console properties found for this Google account. \" +\n \"Make sure you signed in with an account that has access to at least one property.\",\n );\n }\n\n return sites;\n}\n\n// ============================================\n// PICKER\n// ============================================\n\nasync function pickSite(sites: GscSite[], presetSiteUrl?: string): Promise<GscSite> {\n if (presetSiteUrl) {\n const match = sites.find((s) => s.siteUrl === presetSiteUrl);\n if (!match) {\n throw new Error(\n `--site-url \"${presetSiteUrl}\" was not found among ${sites.length} accessible properties. ` +\n `Remove the flag to pick interactively.`,\n );\n }\n return match;\n }\n\n if (sites.length === 1) {\n process.stderr.write(`\\nOnly one property accessible: ${sites[0].siteUrl}. Auto-selecting.\\n`);\n return sites[0];\n }\n\n const choices = sites.map((site) => ({\n title: `${site.siteUrl} (${site.permissionLevel})`,\n value: site,\n }));\n\n const response = await prompts(\n {\n type: \"select\",\n name: \"site\",\n message: \"Which Search Console property should Claude use by default?\",\n choices,\n initial: 0,\n },\n {\n onCancel: () => { throw new Error(\"Cancelled by user\"); },\n },\n );\n\n if (!response.site) throw new Error(\"No site selected\");\n return response.site as GscSite;\n}\n\n// ============================================\n// MAIN\n// ============================================\n\nexport async function run(argv: string[] = process.argv.slice(2)): Promise<void> {\n const args = parseArgs(argv);\n if (args.help) {\n printHelp();\n return;\n }\n\n const clientId = process.env.GOOGLE_GSC_CLIENT_ID?.trim() || EMBEDDED_CLIENT_ID;\n const clientSecret = process.env.GOOGLE_GSC_CLIENT_SECRET?.trim() || EMBEDDED_CLIENT_SECRET;\n\n if (!clientId || !clientSecret) {\n process.stderr.write(\n \"This build of mcp-gsc was published without embedded OAuth credentials.\\n\" +\n \"Set GOOGLE_GSC_CLIENT_ID and GOOGLE_GSC_CLIENT_SECRET in your environment.\\n\",\n );\n process.exit(2);\n }\n\n const port = await findFreeLoopbackPort();\n const redirectUri = `http://127.0.0.1:${port}`;\n const state = randomState();\n const authUrl = buildAuthUrl(clientId, redirectUri, state);\n\n process.stderr.write(\"\\n=== mcp-gsc authentication ===\\n\");\n\n const { code } = await waitForAuthorizationCode(port, state, authUrl);\n process.stderr.write(\"Authorization code received. Exchanging for tokens...\\n\");\n\n const tokens = await exchangeCodeForTokens(code, clientId, clientSecret, redirectUri);\n if (!tokens.refresh_token) {\n throw new GscAuthError(\n \"Google did not return a refresh token. This can happen if you previously granted consent \" +\n \"to this app. Revoke access at https://myaccount.google.com/permissions and try again.\",\n );\n }\n process.stderr.write(\"Tokens received. Fetching accessible Search Console properties...\\n\");\n\n const sites = await enumerateSites(tokens.access_token);\n const chosen = await pickSite(sites, args.siteUrl);\n\n const stored: StoredCredentials = {\n version: CREDENTIALS_FILE_VERSION,\n refresh_token: tokens.refresh_token,\n site_urls: sites.map((s) => s.siteUrl),\n primary_site_url: chosen.siteUrl,\n obtained_at: new Date().toISOString(),\n scopes: [OAUTH_SCOPE],\n };\n writeStoredCredentials(stored);\n\n process.stderr.write(\n [\n \"\",\n \"Done.\",\n \"\",\n ` Property: ${chosen.siteUrl}`,\n ` Permission: ${chosen.permissionLevel}`,\n ` Saved to: ${credentialsFilePath}`,\n \"\",\n \"Next step: fully quit Claude Desktop (Cmd+Q / File > Exit) and reopen it.\",\n 'Then try: \"List my Search Console properties\"',\n \"\",\n ].join(\"\\n\"),\n );\n}\n\nfunction randomState(): string {\n const bytes = new Uint8Array(16);\n (globalThis.crypto as Crypto).getRandomValues(bytes);\n return Array.from(bytes).map((b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\n// ============================================\n// ENTRY\n// ============================================\n\n// Always run when loaded as an entry point. The bin symlink name (mcp-gsc-auth)\n// differs from the file name (auth-cli.js), so import.meta.url checks don't\n// work reliably under npx. Since this module has no side effects when imported\n// as a library (run() must be called explicitly), unconditional execution is safe.\nrun().catch((err) => {\n const classified = classifyError(err);\n process.stderr.write(`\\n${classified.message}\\n`);\n process.exit(1);\n});\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|
package/dist/credentials.js.map
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../src/credentials.ts"],
|
|
4
|
-
"sourcesContent": ["// ============================================\n// CREDENTIAL LOADING & PERSISTENCE\n// ============================================\n// Priority order:\n// 1. config.json + service account (existing multi-client setups)\n// 2. GOOGLE_GSC_* env vars (explicit override)\n// 3. Per-user OAuth credentials file (written by mcp-gsc-auth)\n// 4. EMBEDDED_* constants (client_id, client_secret from build-time)\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from \"fs\";\nimport path from \"path\";\nimport { EMBEDDED_CLIENT_ID, EMBEDDED_CLIENT_SECRET } from \"./embedded-secrets.js\";\nimport { configDir, credentialsFilePath } from \"./platform.js\";\nimport { logger } from \"./resilience.js\";\n\nexport const CREDENTIALS_FILE_VERSION = 1;\n\nexport interface StoredCredentials {\n version: number;\n refresh_token: string;\n site_urls: string[];\n primary_site_url?: string;\n obtained_at: string;\n scopes: string[];\n}\n\nexport interface ResolvedOAuthCredentials {\n client_id: string;\n client_secret: string;\n refresh_token: string;\n site_urls: string[];\n primary_site_url: string;\n source: \"env\" | \"file\" | \"mixed\";\n}\n\nconst envTrimmed = (key: string): string =>\n (process.env[key] || \"\").trim().replace(/^[\"']|[\"']$/g, \"\");\n\n// ============================================\n// FILE I/O\n// ============================================\n\nexport function readStoredCredentials(filePath: string = credentialsFilePath): StoredCredentials | null {\n if (!existsSync(filePath)) return null;\n try {\n const raw = readFileSync(filePath, \"utf-8\");\n const parsed = JSON.parse(raw);\n if (parsed.version !== CREDENTIALS_FILE_VERSION) {\n logger.warn(\n { path: filePath, version: parsed.version, expected: CREDENTIALS_FILE_VERSION },\n \"Credentials file version mismatch\",\n );\n return null;\n }\n return parsed as StoredCredentials;\n } catch (err) {\n logger.warn({ err, path: filePath }, \"Failed to parse credentials file\");\n return null;\n }\n}\n\nexport function writeStoredCredentials(\n creds: StoredCredentials,\n filePath: string = credentialsFilePath,\n): void {\n const dir = path.dirname(filePath);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n writeFileSync(filePath, JSON.stringify(creds, null, 2), { encoding: \"utf-8\" });\n try {\n chmodSync(filePath, 0o600);\n } catch {\n // Best-effort on POSIX\n }\n}\n\n// ============================================\n// RESOLVE (read-time priority chain)\n// ============================================\n\nexport function resolveOAuthCredentials(\n credsFilePath: string = credentialsFilePath,\n): ResolvedOAuthCredentials {\n const client_id = envTrimmed(\"GOOGLE_GSC_CLIENT_ID\") || EMBEDDED_CLIENT_ID;\n const client_secret = envTrimmed(\"GOOGLE_GSC_CLIENT_SECRET\") || EMBEDDED_CLIENT_SECRET;\n\n const stored = readStoredCredentials(credsFilePath);\n const envRefresh = envTrimmed(\"GOOGLE_GSC_REFRESH_TOKEN\");\n const envSiteUrl = envTrimmed(\"GOOGLE_GSC_SITE_URL\");\n\n const refresh_token = envRefresh || stored?.refresh_token || \"\";\n const site_urls = stored?.site_urls || [];\n const primary_site_url = envSiteUrl || stored?.primary_site_url || site_urls[0] || \"\";\n\n const source: ResolvedOAuthCredentials[\"source\"] =\n envRefresh && stored ? \"mixed\" : envRefresh ? \"env\" : stored ? \"file\" : \"env\";\n\n const missing: string[] = [];\n if (!client_id) missing.push(\"client_id\");\n if (!client_secret) missing.push(\"client_secret\");\n if (!refresh_token) missing.push(\"refresh_token\");\n\n if (missing.length > 0) {\n throw new Error(buildMissingCredentialsMessage(missing, Boolean(stored)));\n }\n\n return { client_id, client_secret, refresh_token, site_urls, primary_site_url, source };\n}\n\nfunction buildMissingCredentialsMessage(missing: string[], hasFile: boolean): string {\n const runAuth = \"npx mcp-gsc-auth\";\n const lines: string[] = [\n `Missing GSC OAuth credentials: ${missing.join(\", \")}.`,\n ``,\n `To get started, run:`,\n ` ${runAuth}`,\n ``,\n `This will open your browser, walk you through Google sign-in, let you pick which`,\n `Search Console property to use, and save the result to:`,\n ` ${credentialsFilePath}`,\n ];\n if (hasFile) {\n lines.push(\n ``,\n `A credentials file exists at ${credentialsFilePath} but is missing required fields.`,\n `Re-run the auth helper to refresh it.`,\n );\n }\n lines.push(\n ``,\n `Advanced: you can bypass the auth helper by setting these env vars:`,\n ` GOOGLE_GSC_CLIENT_ID, GOOGLE_GSC_CLIENT_SECRET, GOOGLE_GSC_REFRESH_TOKEN`,\n ``,\n `Or use a service account: set GOOGLE_APPLICATION_CREDENTIALS to a JSON key file.`,\n );\n return lines.join(\"\\n\");\n}\n\nexport { configDir, credentialsFilePath } from \"./platform.js\";\n"],
|
|
5
|
-
"mappings": "AASA,SAAS,YAAY,WAAW,cAAc,eAAe,iBAAiB;AAC9E,OAAO,UAAU;AACjB,SAAS,oBAAoB,8BAA8B;AAC3D,SAAoB,2BAA2B;AAC/C,SAAS,cAAc;AAEhB,MAAM,2BAA2B;AAoBxC,MAAM,aAAa,CAAC,SACjB,QAAQ,IAAI,GAAG,KAAK,IAAI,KAAK,EAAE,QAAQ,gBAAgB,EAAE;AAMrD,SAAS,sBAAsB,WAAmB,qBAA+C;AACtG,MAAI,CAAC,WAAW,QAAQ,EAAG,QAAO;AAClC,MAAI;AACF,UAAM,MAAM,aAAa,UAAU,OAAO;AAC1C,UAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,QAAI,OAAO,YAAY,0BAA0B;AAC/C,aAAO;AAAA,QACL,EAAE,MAAM,UAAU,SAAS,OAAO,SAAS,UAAU,yBAAyB;AAAA,QAC9E;AAAA,MACF;AACA,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT,SAAS,KAAK;AACZ,WAAO,KAAK,EAAE,KAAK,MAAM,SAAS,GAAG,kCAAkC;AACvE,WAAO;AAAA,EACT;AACF;AAEO,SAAS,uBACd,OACA,WAAmB,qBACb;AACN,QAAM,MAAM,KAAK,QAAQ,QAAQ;AACjC,MAAI,CAAC,WAAW,GAAG,GAAG;AACpB,cAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAAA,EACpC;AACA,gBAAc,UAAU,KAAK,UAAU,OAAO,MAAM,CAAC,GAAG,EAAE,UAAU,QAAQ,CAAC;AAC7E,MAAI;AACF,cAAU,UAAU,GAAK;AAAA,EAC3B,QAAQ;AAAA,EAER;AACF;AAMO,SAAS,wBACd,gBAAwB,qBACE;AAC1B,QAAM,YAAY,WAAW,sBAAsB,KAAK;AACxD,QAAM,gBAAgB,WAAW,0BAA0B,KAAK;AAEhE,QAAM,SAAS,sBAAsB,aAAa;AAClD,QAAM,aAAa,WAAW,0BAA0B;AACxD,QAAM,aAAa,WAAW,qBAAqB;AAEnD,QAAM,gBAAgB,cAAc,QAAQ,iBAAiB;AAC7D,QAAM,YAAY,QAAQ,aAAa,CAAC;AACxC,QAAM,mBAAmB,cAAc,QAAQ,oBAAoB,UAAU,CAAC,KAAK;AAEnF,QAAM,SACJ,cAAc,SAAS,UAAU,aAAa,QAAQ,SAAS,SAAS;AAE1E,QAAM,UAAoB,CAAC;AAC3B,MAAI,CAAC,UAAW,SAAQ,KAAK,WAAW;AACxC,MAAI,CAAC,cAAe,SAAQ,KAAK,eAAe;AAChD,MAAI,CAAC,cAAe,SAAQ,KAAK,eAAe;AAEhD,MAAI,QAAQ,SAAS,GAAG;AACtB,UAAM,IAAI,MAAM,+BAA+B,SAAS,QAAQ,MAAM,CAAC,CAAC;AAAA,EAC1E;AAEA,SAAO,EAAE,WAAW,eAAe,eAAe,WAAW,kBAAkB,OAAO;AACxF;AAEA,SAAS,+BAA+B,SAAmB,SAA0B;AACnF,QAAM,UAAU;AAChB,QAAM,QAAkB;AAAA,IACtB,kCAAkC,QAAQ,KAAK,IAAI,CAAC;AAAA,IACpD;AAAA,IACA;AAAA,IACA,OAAO,OAAO;AAAA,IACd;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO,mBAAmB;AAAA,EAC5B;AACA,MAAI,SAAS;AACX,UAAM;AAAA,MACJ;AAAA,MACA,gCAAgC,mBAAmB;AAAA,MACnD;AAAA,IACF;AAAA,EACF;AACA,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACA,SAAO,MAAM,KAAK,IAAI;AACxB;AAEA,SAAS,aAAAA,YAAW,uBAAAC,4BAA2B;",
|
|
6
|
-
"names": ["configDir", "credentialsFilePath"]
|
|
7
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../src/embedded-secrets.ts"],
|
|
4
|
-
"sourcesContent": ["// ============================================\n// BUILD-TIME INJECTED SECRETS\n// ============================================\n// Replaced by esbuild --define at build time. In dev mode (tsx),\n// falls back to runtime env vars via credentials.ts.\n//\n// GSC only needs client_id + client_secret (no developer token).\n// Google Desktop OAuth client ID/secret are NOT true secrets per Google docs.\n\nexport const EMBEDDED_CLIENT_ID: string = process.env.EMBEDDED_CLIENT_ID || \"\";\nexport const EMBEDDED_CLIENT_SECRET: string = process.env.EMBEDDED_CLIENT_SECRET || \"\";\n\nexport function hasEmbeddedSecrets(): boolean {\n return EMBEDDED_CLIENT_ID.length > 10 && EMBEDDED_CLIENT_SECRET.length > 10;\n}\n"],
|
|
5
|
-
"mappings": "AASO,MAAM,qBAA6B;AACnC,MAAM,yBAAiC;AAEvC,SAAS,qBAA8B;AAC5C,SAAO,mBAAmB,SAAS,MAAM,uBAAuB,SAAS;AAC3E;",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|
package/dist/errors.js.map
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../src/errors.ts"],
|
|
4
|
-
"sourcesContent": ["// ============================================\n// TYPED ERRORS\n// ============================================\n\nexport class GscAuthError extends Error {\n constructor(message: string, public readonly cause?: unknown) {\n super(message);\n this.name = \"GscAuthError\";\n }\n}\n\nexport class GscRateLimitError extends Error {\n constructor(\n public readonly retryAfterMs: number,\n cause?: unknown,\n ) {\n super(`GSC rate limited, retry after ${retryAfterMs}ms`);\n this.name = \"GscRateLimitError\";\n this.cause = cause;\n }\n}\n\nexport class GscServiceError extends Error {\n constructor(message: string, public readonly cause?: unknown) {\n super(message);\n this.name = \"GscServiceError\";\n }\n}\n\n// ============================================\n// STARTUP CREDENTIAL VALIDATION\n// ============================================\n\nexport function validateCredentials(credentialsFile: string): { valid: boolean; missing: string[] } {\n const missing: string[] = [];\n\n if (!credentialsFile || credentialsFile.trim() === \"\") {\n missing.push(\"credentials_file (in config.json or GOOGLE_APPLICATION_CREDENTIALS env var)\");\n } else if (credentialsFile.trim().length > 0 && credentialsFile.trim().length < 5) {\n // Basic format validation: path should have reasonable length > 5 chars\n missing.push(\"credentials_file (format: path too short, expected length > 5)\");\n }\n\n return { valid: missing.length === 0, missing };\n}\n\nexport function classifyError(error: any): Error {\n const message = error?.message || String(error);\n const status = error?.code || error?.status;\n // Check response body for error objects (REST API can return errors in body on 200)\n const bodyError = error?.response?.body?.error || error?.data?.error || error?.errors?.[0];\n\n if (\n status === 401 ||\n status === 403 ||\n message.includes(\"invalid_grant\") ||\n message.includes(\"PERMISSION_DENIED\") ||\n message.includes(\"access_denied\") ||\n message.includes(\"Invalid credentials\") ||\n bodyError?.code === 403\n ) {\n return new GscAuthError(\n `GSC auth failed: ${message}. Check service account credentials and permissions.`,\n error,\n );\n }\n\n if (status === 429 || message.includes(\"rateLimitExceeded\") || message.includes(\"RESOURCE_EXHAUSTED\")) {\n const retryMs = 60_000;\n return new GscRateLimitError(retryMs, error);\n }\n\n if (status >= 500 || message.includes(\"INTERNAL\") || message.includes(\"UNAVAILABLE\")) {\n return new GscServiceError(`GSC API server error: ${message}`, error);\n }\n\n return error;\n}\n"],
|
|
5
|
-
"mappings": "AAIO,MAAM,qBAAqB,MAAM;AAAA,EACtC,YAAY,SAAiC,OAAiB;AAC5D,UAAM,OAAO;AAD8B;AAE3C,SAAK,OAAO;AAAA,EACd;AACF;AAEO,MAAM,0BAA0B,MAAM;AAAA,EAC3C,YACkB,cAChB,OACA;AACA,UAAM,iCAAiC,YAAY,IAAI;AAHvC;AAIhB,SAAK,OAAO;AACZ,SAAK,QAAQ;AAAA,EACf;AACF;AAEO,MAAM,wBAAwB,MAAM;AAAA,EACzC,YAAY,SAAiC,OAAiB;AAC5D,UAAM,OAAO;AAD8B;AAE3C,SAAK,OAAO;AAAA,EACd;AACF;AAMO,SAAS,oBAAoB,iBAAgE;AAClG,QAAM,UAAoB,CAAC;AAE3B,MAAI,CAAC,mBAAmB,gBAAgB,KAAK,MAAM,IAAI;AACrD,YAAQ,KAAK,6EAA6E;AAAA,EAC5F,WAAW,gBAAgB,KAAK,EAAE,SAAS,KAAK,gBAAgB,KAAK,EAAE,SAAS,GAAG;AAEjF,YAAQ,KAAK,gEAAgE;AAAA,EAC/E;AAEA,SAAO,EAAE,OAAO,QAAQ,WAAW,GAAG,QAAQ;AAChD;AAEO,SAAS,cAAc,OAAmB;AAC/C,QAAM,UAAU,OAAO,WAAW,OAAO,KAAK;AAC9C,QAAM,SAAS,OAAO,QAAQ,OAAO;AAErC,QAAM,YAAY,OAAO,UAAU,MAAM,SAAS,OAAO,MAAM,SAAS,OAAO,SAAS,CAAC;AAEzF,MACE,WAAW,OACX,WAAW,OACX,QAAQ,SAAS,eAAe,KAChC,QAAQ,SAAS,mBAAmB,KACpC,QAAQ,SAAS,eAAe,KAChC,QAAQ,SAAS,qBAAqB,KACtC,WAAW,SAAS,KACpB;AACA,WAAO,IAAI;AAAA,MACT,oBAAoB,OAAO;AAAA,MAC3B;AAAA,IACF;AAAA,EACF;AAEA,MAAI,WAAW,OAAO,QAAQ,SAAS,mBAAmB,KAAK,QAAQ,SAAS,oBAAoB,GAAG;AACrG,UAAM,UAAU;AAChB,WAAO,IAAI,kBAAkB,SAAS,KAAK;AAAA,EAC7C;AAEA,MAAI,UAAU,OAAO,QAAQ,SAAS,UAAU,KAAK,QAAQ,SAAS,aAAa,GAAG;AACpF,WAAO,IAAI,gBAAgB,yBAAyB,OAAO,IAAI,KAAK;AAAA,EACtE;AAEA,SAAO;AACT;",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|
package/dist/index.js.map
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../src/index.ts"],
|
|
4
|
-
"sourcesContent": ["#!/usr/bin/env node\n\nimport { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport {\n CallToolRequestSchema,\n ListToolsRequestSchema,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport { readFileSync, existsSync } from \"fs\";\nimport { join, dirname, resolve, isAbsolute } from \"path\";\nimport { google, searchconsole_v1 } from \"googleapis\";\nimport { GoogleAuth, OAuth2Client } from \"googleapis-common\";\nimport {\n GscAuthError,\n GscRateLimitError,\n GscServiceError,\n classifyError,\n validateCredentials,\n} from \"./errors.js\";\nimport { resolveOAuthCredentials } from \"./credentials.js\";\nimport { EMBEDDED_CLIENT_ID, EMBEDDED_CLIENT_SECRET } from \"./embedded-secrets.js\";\nimport { tools } from \"./tools.js\";\nimport { withResilience, safeResponse, logger } from \"./resilience.js\";\nimport { checkForUpdate } from \"./updateNotifier.js\";\nimport v8 from \"v8\";\n\n// CLI package info\nconst __cliPkg = JSON.parse(readFileSync(join(dirname(new URL(import.meta.url).pathname), \"..\", \"package.json\"), \"utf-8\"));\n\n// Log build fingerprint at startup\ntry {\n const __buildInfoDir = dirname(new URL(import.meta.url).pathname);\n const buildInfo = JSON.parse(readFileSync(join(__buildInfoDir, \"build-info.json\"), \"utf-8\"));\n console.error(`[build] SHA: ${buildInfo.sha} (${buildInfo.builtAt})`);\n} catch {\n console.error(`[build] ${__cliPkg.name}@${__cliPkg.version} (dev mode)`);\n}\n\n// Version safety: warn if running a deprecated or dangerously old version\nconst __minimumSafeVersion = \"1.0.5\"; // minimum version with input sanitization\nconst __semverLt = (a: string, b: string) => { const pa = a.split(\".\").map(Number), pb = b.split(\".\").map(Number); for (let i = 0; i < 3; i++) { if ((pa[i] || 0) < (pb[i] || 0)) return true; if ((pa[i] || 0) > (pb[i] || 0)) return false; } return false; };\nif (__semverLt(__cliPkg.version, __minimumSafeVersion)) {\n console.error(`[WARNING] Running deprecated version ${__cliPkg.version}. Minimum safe version is ${__minimumSafeVersion}. Please upgrade.`);\n}\n\n// Fire-and-forget npm outdated check. Non-blocking; any error is swallowed.\nvoid checkForUpdate(__cliPkg.name, __cliPkg.version).catch(() => {});\n\n// CLI flags\nif (process.argv.includes(\"--help\") || process.argv.includes(\"-h\")) {\n console.error(`${__cliPkg.name} v${__cliPkg.version}\\n`);\n console.error(`Usage: ${__cliPkg.name} [options]\\n`);\n console.error(\"MCP server communicating via stdio. Configure in your .mcp.json.\\n\");\n console.error(\"Options:\");\n console.error(\" --help, -h Show this help message\");\n console.error(\" --version, -v Show version number\");\n console.error(`\\nDocumentation: https://github.com/mharnett/mcp-search-console`);\n process.exit(0);\n}\nif (process.argv.includes(\"--version\") || process.argv.includes(\"-v\")) {\n console.error(__cliPkg.version);\n process.exit(0);\n}\n\n// Startup: detect npx vs direct node\nif (process.argv[1]?.includes('.npm/_npx')) {\n console.error(\"[startup] Running via npx -- first run may be slow due to package resolution\");\n}\n\n// Startup: check heap size\nconst heapLimit = v8.getHeapStatistics().heap_size_limit;\nif (heapLimit < 256 * 1024 * 1024) {\n console.error(`[startup] WARNING: Heap limit is ${Math.round(heapLimit / 1024 / 1024)}MB`);\n}\n\n// ============================================\n// ENV VAR TRIMMING\n// ============================================\n\nconst envTrimmed = (key: string): string => (process.env[key] || \"\").trim().replace(/^[\"']|[\"']$/g, \"\");\n\n// ============================================\n// CONFIGURATION\n// ============================================\n\ninterface ClientConfig {\n name: string;\n folder: string;\n site_url: string;\n}\n\ninterface Config {\n credentials_file: string;\n clients: Record<string, ClientConfig>;\n}\n\nfunction loadConfig(): Config {\n // Try config.json (for multi-client setups)\n const configPath = join(dirname(new URL(import.meta.url).pathname), \"..\", \"config.json\");\n if (existsSync(configPath)) {\n const raw = JSON.parse(readFileSync(configPath, \"utf-8\"));\n const rawCf = raw.credentials_file || envTrimmed(\"GOOGLE_APPLICATION_CREDENTIALS\");\n return {\n credentials_file: rawCf && !isAbsolute(rawCf) ? resolve(rawCf) : rawCf,\n clients: raw.clients || {},\n };\n }\n\n // Fall back to env vars for service account\n const rawCredsFile = envTrimmed(\"GOOGLE_APPLICATION_CREDENTIALS\");\n const credsFile = rawCredsFile && !isAbsolute(rawCredsFile) ? resolve(rawCredsFile) : rawCredsFile;\n if (credsFile) {\n return {\n credentials_file: credsFile,\n clients: {},\n };\n }\n\n // Fall back to OAuth credentials (from mcp-gsc-auth or env vars)\n // Return empty credentials_file to signal OAuth mode\n return {\n credentials_file: \"\",\n clients: {},\n };\n}\n\nfunction getClientFromWorkingDir(config: Config, cwd: string): ClientConfig | null {\n for (const [key, client] of Object.entries(config.clients)) {\n if (cwd.startsWith(client.folder) || cwd.includes(key)) {\n return client;\n }\n }\n return null;\n}\n\nfunction getDefaultSiteUrl(config: Config): string | null {\n const clients = Object.values(config.clients);\n return clients.length > 0 ? clients[0].site_url : null;\n}\n\n// ============================================\n// DATE HELPERS\n// ============================================\n\n// Note: resolveDate uses UTC dates via toISOString(). GSC data is in the property's timezone.\n// At 11PM PT, \"today\" resolves to tomorrow in UTC. Users should be aware of this timezone behavior.\nfunction resolveDate(dateStr: string): string {\n const today = new Date();\n if (dateStr === \"today\") {\n return today.toISOString().slice(0, 10);\n }\n const match = dateStr.match(/^(\\d+)daysAgo$/);\n if (match) {\n const days = parseInt(match[1], 10);\n const d = new Date(today);\n d.setDate(d.getDate() - days);\n return d.toISOString().slice(0, 10);\n }\n return dateStr; // assume YYYY-MM-DD\n}\n\n// ============================================\n// DIMENSION FILTER PARSING\n// ============================================\n\ninterface DimensionFilter {\n dimension: string;\n operator: string;\n expression: string;\n}\n\nfunction parseDimensionFilter(filterStr: string): DimensionFilter | null {\n if (!filterStr) return null;\n\n const operators = [\n \"includingRegex\", \"excludingRegex\",\n \"notContains\", \"notEquals\",\n \"contains\", \"equals\",\n ];\n\n for (const op of operators) {\n const parts = filterStr.split(` ${op} `, 2);\n if (parts.length === 2) {\n return {\n dimension: parts[0].trim(),\n operator: op,\n expression: parts[1].trim(),\n };\n }\n }\n\n return null;\n}\n\n// ============================================\n// GOOGLE SEARCH CONSOLE API CLIENT\n// ============================================\n\nclass GscManager {\n private config: Config;\n private service: searchconsole_v1.Searchconsole | null = null;\n private authMode: \"service_account\" | \"oauth\" = \"service_account\";\n\n constructor(config: Config) {\n this.config = config;\n\n if (config.credentials_file) {\n // Service account mode\n const creds = validateCredentials(config.credentials_file);\n if (!creds.valid) {\n const msg = `[STARTUP ERROR] Missing required credentials: ${creds.missing.join(\", \")}. MCP will not function.`;\n console.error(msg);\n throw new GscAuthError(msg);\n }\n this.authMode = \"service_account\";\n } else {\n // OAuth mode -- resolve will throw with helpful message if no credentials found\n resolveOAuthCredentials(); // validates credentials exist\n this.authMode = \"oauth\";\n }\n }\n\n private getService(): searchconsole_v1.Searchconsole {\n if (!this.service) {\n if (this.authMode === \"service_account\") {\n const auth = new google.auth.GoogleAuth({\n keyFile: this.config.credentials_file,\n scopes: [\"https://www.googleapis.com/auth/webmasters.readonly\"],\n });\n this.service = google.searchconsole({ version: \"v1\", auth });\n console.error(`[startup] Service account loaded from: ${this.config.credentials_file}`);\n } else {\n const resolved = resolveOAuthCredentials();\n const oauth2Client = new google.auth.OAuth2(\n resolved.client_id,\n resolved.client_secret,\n );\n oauth2Client.setCredentials({ refresh_token: resolved.refresh_token });\n this.service = google.searchconsole({ version: \"v1\", auth: oauth2Client });\n console.error(`[startup] OAuth credentials loaded (source: ${resolved.source})`);\n }\n }\n return this.service;\n }\n\n async listSites(): Promise<any> {\n const svc = this.getService();\n return withResilience(async () => {\n const resp = await svc.sites.list();\n const sites = (resp.data.siteEntry || []).map((entry) => ({\n site_url: entry.siteUrl || \"\",\n permission_level: entry.permissionLevel || \"\",\n }));\n return { sites, count: sites.length };\n }, \"gsc_list_sites\");\n }\n\n async searchAnalytics(options: {\n startDate: string;\n endDate: string;\n dimensions: string[];\n searchType: string;\n dimensionFilter: string;\n rowLimit: number;\n aggregationType: string;\n siteUrl: string;\n }): Promise<any> {\n const svc = this.getService();\n const siteUrl = options.siteUrl || getDefaultSiteUrl(this.config);\n if (!siteUrl) {\n return { error: \"No site_url provided and none found in config\" };\n }\n\n const rowLimit = Math.min(Math.max(1, options.rowLimit), 25000);\n const startDate = resolveDate(options.startDate);\n const endDate = resolveDate(options.endDate);\n\n // Future date validation (skip relative dates like \"90daysAgo\")\n const today_gsc = new Date().toISOString().slice(0, 10);\n if (startDate && !options.startDate.includes(\"daysAgo\") && !options.startDate.includes(\"yesterday\") && !options.startDate.includes(\"today\") && startDate > today_gsc) {\n return { error: `start_date \"${startDate}\" is in the future. Reports only cover historical data.` };\n }\n\n const requestBody: any = {\n startDate,\n endDate,\n dimensions: options.dimensions,\n type: options.searchType,\n rowLimit,\n aggregationType: options.aggregationType,\n };\n\n const parsed = parseDimensionFilter(options.dimensionFilter);\n if (parsed) {\n requestBody.dimensionFilterGroups = [{\n filters: [{\n dimension: parsed.dimension,\n operator: parsed.operator,\n expression: parsed.expression,\n }],\n }];\n }\n\n return withResilience(async () => {\n const resp = await svc.searchanalytics.query({\n siteUrl,\n requestBody,\n });\n\n const rows = (resp.data.rows || []).map((row) => {\n const r: Record<string, any> = {};\n for (let i = 0; i < options.dimensions.length; i++) {\n if (row.keys && i < row.keys.length) {\n r[options.dimensions[i]] = row.keys[i];\n }\n }\n r.clicks = row.clicks || 0;\n r.impressions = row.impressions || 0;\n r.ctr = Math.round((row.ctr || 0) * 10000) / 10000;\n r.position = Math.round((row.position || 0) * 10) / 10;\n return r;\n });\n\n return {\n rows,\n row_count: rows.length,\n date_range: `${startDate} to ${endDate}`,\n site_url: siteUrl,\n };\n }, \"gsc_search_analytics\");\n }\n\n async inspection(url: string, siteUrl: string): Promise<any> {\n const svc = this.getService();\n const resolvedSiteUrl = siteUrl || getDefaultSiteUrl(this.config);\n if (!resolvedSiteUrl) {\n return { error: \"No site_url provided and none found in config\" };\n }\n\n return withResilience(async () => {\n try {\n const resp = await svc.urlInspection.index.inspect({\n requestBody: {\n inspectionUrl: url,\n siteUrl: resolvedSiteUrl,\n },\n });\n\n const result = resp.data.inspectionResult || {};\n const indexStatus = result.indexStatusResult || {};\n const mobile = result.mobileUsabilityResult || {};\n const rich = result.richResultsResult || {};\n\n return {\n url,\n site_url: resolvedSiteUrl,\n index_status: {\n verdict: indexStatus.verdict || \"UNKNOWN\",\n coverage_state: indexStatus.coverageState || \"\",\n indexing_state: indexStatus.indexingState || \"\",\n last_crawl_time: indexStatus.lastCrawlTime || \"\",\n page_fetch_state: indexStatus.pageFetchState || \"\",\n robots_txt_state: indexStatus.robotsTxtState || \"\",\n crawled_as: indexStatus.crawledAs || \"\",\n referring_urls: indexStatus.referringUrls || [],\n },\n mobile_usability: {\n verdict: mobile.verdict || \"UNKNOWN\",\n issues: (mobile.issues || []).map((i: any) => i.issueType || \"\"),\n },\n rich_results: {\n verdict: rich.verdict || \"UNKNOWN\",\n },\n };\n } catch (err) {\n return { error: String(err), url, site_url: resolvedSiteUrl };\n }\n }, \"gsc_inspection\");\n }\n}\n\n// ============================================\n// MCP SERVER\n// ============================================\n\nconst config = loadConfig();\nconst gscManager = new GscManager(config);\n\nconst server = new Server(\n {\n name: __cliPkg.name,\n version: __cliPkg.version,\n },\n {\n capabilities: {\n tools: {},\n },\n }\n);\n\nserver.setRequestHandler(ListToolsRequestSchema, async () => {\n return { tools };\n});\n\nserver.setRequestHandler(CallToolRequestSchema, async (request) => {\n const { name, arguments: args } = request.params;\n\n try {\n switch (name) {\n case \"gsc_get_client_context\": {\n const cwd = args?.working_directory as string;\n const client = getClientFromWorkingDir(config, cwd);\n if (!client) {\n return {\n content: [{\n type: \"text\",\n text: JSON.stringify({\n error: \"No client found for working directory\",\n working_directory: cwd,\n available_clients: Object.entries(config.clients).map(([k, v]) => ({\n key: k,\n name: v.name,\n folder: v.folder,\n })),\n }, null, 2),\n }],\n };\n }\n return {\n content: [{\n type: \"text\",\n text: JSON.stringify({\n client_name: client.name,\n site_url: client.site_url,\n folder: client.folder,\n }, null, 2),\n }],\n };\n }\n\n case \"gsc_list_sites\": {\n const result = await gscManager.listSites();\n return {\n content: [{\n type: \"text\",\n text: JSON.stringify(safeResponse(result, \"listSites\"), null, 2),\n }],\n };\n }\n\n case \"gsc_search_analytics\": {\n const dimensions = ((args?.dimensions as string) || \"query\")\n .split(\",\")\n .map((d: string) => d.trim())\n .filter(Boolean);\n\n const result = await gscManager.searchAnalytics({\n startDate: (args?.start_date as string) || \"90daysAgo\",\n endDate: (args?.end_date as string) || \"today\",\n dimensions,\n searchType: (args?.search_type as string) || \"web\",\n dimensionFilter: (args?.dimension_filter as string) || \"\",\n rowLimit: (args?.row_limit as number) || 100,\n aggregationType: (args?.aggregation_type as string) || \"auto\",\n siteUrl: (args?.site_url as string) || \"\",\n });\n\n return {\n content: [{\n type: \"text\",\n text: JSON.stringify(safeResponse(result, \"searchAnalytics\"), null, 2),\n }],\n };\n }\n\n case \"gsc_inspection\": {\n const result = await gscManager.inspection(\n args?.url as string,\n (args?.site_url as string) || \"\",\n );\n return {\n content: [{\n type: \"text\",\n text: JSON.stringify(result, null, 2),\n }],\n };\n }\n\n default:\n throw new Error(`Unknown tool: ${name}`);\n }\n } catch (rawError: any) {\n const error = classifyError(rawError);\n logger.error({ error_type: error.name, message: error.message }, \"Tool call failed\");\n\n const response: Record<string, unknown> = {\n error: true,\n error_type: error.name,\n message: error.message,\n server: __cliPkg.name,\n };\n\n if (error instanceof GscAuthError) {\n response.action_required = \"Check credentials (service account or OAuth) and Search Console permissions. If using OAuth, re-run: npx mcp-gsc-auth\";\n } else if (error instanceof GscRateLimitError) {\n response.retry_after_ms = error.retryAfterMs;\n response.action_required = `Rate limited. Retry after ${Math.ceil(error.retryAfterMs / 1000)} seconds.`;\n } else if (error instanceof GscServiceError) {\n response.action_required = \"Google Search Console API server error. This is transient - retry in a few minutes.\";\n } else {\n response.details = rawError.stack;\n }\n\n // Size-limit error responses through safeResponse to prevent oversized payloads\n const safeErrorResponse = safeResponse(response, \"error\");\n return {\n isError: true,\n content: [{\n type: \"text\",\n text: JSON.stringify(safeErrorResponse, null, 2),\n }],\n };\n }\n});\n\n// Start server\nasync function main() {\n try {\n await gscManager.listSites();\n console.error(\"[startup] Auth verified: GSC API call succeeded\");\n } catch (err: any) {\n console.error(`[STARTUP WARNING] Auth check FAILED: ${err.message}`);\n console.error(`[STARTUP WARNING] MCP will start but API calls may fail until auth is fixed.`);\n }\n\n const transport = new StdioServerTransport();\n await server.connect(transport);\n console.error(\"[startup] MCP GSC server running\");\n}\n\nprocess.on(\"SIGTERM\", () => {\n console.error(\"[shutdown] SIGTERM received, exiting\");\n process.exit(0);\n});\n\nprocess.on(\"SIGINT\", () => {\n console.error(\"[shutdown] SIGINT received, exiting\");\n process.exit(0);\n});\n\nprocess.on(\"SIGPIPE\", () => {\n // Client disconnected -- expected during shutdown\n});\n\nprocess.on(\"unhandledRejection\", (reason) => {\n console.error(\"[error] Unhandled promise rejection:\", reason);\n});\n\nmain().catch(console.error);\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": ["config"]
|
|
7
|
-
}
|
package/dist/platform.js.map
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../src/platform.ts"],
|
|
4
|
-
"sourcesContent": ["// ============================================\n// CROSS-PLATFORM DISPATCH HELPERS\n// ============================================\n\nimport envPaths from \"env-paths\";\nimport openModule from \"open\";\nimport { createServer } from \"net\";\nimport { platform } from \"os\";\nimport path from \"path\";\n\nconst paths = envPaths(\"mcp-gsc\", { suffix: \"nodejs\" });\n\nexport const configDir = paths.config;\nexport const credentialsFilePath = path.join(paths.config, \"credentials.json\");\n\nexport async function openBrowser(url: string): Promise<void> {\n await openModule(url);\n}\n\nconst LOOPBACK_PORT_RANGE_START = 8085;\nconst LOOPBACK_PORT_RANGE_END = 8199;\n\nexport async function findFreeLoopbackPort(): Promise<number> {\n for (let port = LOOPBACK_PORT_RANGE_START; port <= LOOPBACK_PORT_RANGE_END; port++) {\n if (await isPortFree(port)) {\n return port;\n }\n }\n throw new Error(\n `No free port found in range ${LOOPBACK_PORT_RANGE_START}-${LOOPBACK_PORT_RANGE_END}. ` +\n `Close other apps that might be listening and try again.`,\n );\n}\n\nfunction isPortFree(port: number): Promise<boolean> {\n return new Promise((resolve) => {\n const server = createServer();\n server.once(\"error\", () => resolve(false));\n server.once(\"listening\", () => {\n server.close(() => resolve(true));\n });\n server.listen(port, \"127.0.0.1\");\n });\n}\n\nexport function currentPlatform(): \"darwin\" | \"win32\" | \"linux\" | \"other\" {\n const p = platform();\n if (p === \"darwin\" || p === \"win32\" || p === \"linux\") return p;\n return \"other\";\n}\n\nexport function onPosixSignal(\n signal: \"SIGPIPE\" | \"SIGHUP\" | \"SIGUSR1\" | \"SIGUSR2\" | \"SIGQUIT\",\n handler: () => void,\n): void {\n if (platform() === \"win32\") return;\n process.on(signal as NodeJS.Signals, handler);\n}\n"],
|
|
5
|
-
"mappings": "AAIA,OAAO,cAAc;AACrB,OAAO,gBAAgB;AACvB,SAAS,oBAAoB;AAC7B,SAAS,gBAAgB;AACzB,OAAO,UAAU;AAEjB,MAAM,QAAQ,SAAS,WAAW,EAAE,QAAQ,SAAS,CAAC;AAE/C,MAAM,YAAY,MAAM;AACxB,MAAM,sBAAsB,KAAK,KAAK,MAAM,QAAQ,kBAAkB;AAE7E,eAAsB,YAAY,KAA4B;AAC5D,QAAM,WAAW,GAAG;AACtB;AAEA,MAAM,4BAA4B;AAClC,MAAM,0BAA0B;AAEhC,eAAsB,uBAAwC;AAC5D,WAAS,OAAO,2BAA2B,QAAQ,yBAAyB,QAAQ;AAClF,QAAI,MAAM,WAAW,IAAI,GAAG;AAC1B,aAAO;AAAA,IACT;AAAA,EACF;AACA,QAAM,IAAI;AAAA,IACR,+BAA+B,yBAAyB,IAAI,uBAAuB;AAAA,EAErF;AACF;AAEA,SAAS,WAAW,MAAgC;AAClD,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,UAAM,SAAS,aAAa;AAC5B,WAAO,KAAK,SAAS,MAAM,QAAQ,KAAK,CAAC;AACzC,WAAO,KAAK,aAAa,MAAM;AAC7B,aAAO,MAAM,MAAM,QAAQ,IAAI,CAAC;AAAA,IAClC,CAAC;AACD,WAAO,OAAO,MAAM,WAAW;AAAA,EACjC,CAAC;AACH;AAEO,SAAS,kBAA0D;AACxE,QAAM,IAAI,SAAS;AACnB,MAAI,MAAM,YAAY,MAAM,WAAW,MAAM,QAAS,QAAO;AAC7D,SAAO;AACT;AAEO,SAAS,cACd,QACA,SACM;AACN,MAAI,SAAS,MAAM,QAAS;AAC5B,UAAQ,GAAG,QAA0B,OAAO;AAC9C;",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|