search-console-mcp 1.9.2 → 1.10.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 CHANGED
@@ -1,3 +1,4 @@
1
+
1
2
  # Google Search Console MCP Server
2
3
 
3
4
  A Model Context Protocol (MCP) server that transforms how you interact with Google Search Console. Stop exporting CSVs and start asking questions.
@@ -6,10 +7,6 @@ A Model Context Protocol (MCP) server that transforms how you interact with Goog
6
7
 
7
8
  ---
8
9
 
9
- ### [šŸ  Overview](#) | [šŸŽÆ Prompts](#-magic-prompts) | [šŸš€ Quick Start](#-quick-start) | [šŸ› ļø Tools](#-tools-reference)
10
-
11
- ---
12
-
13
10
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
14
11
  [![Tests](https://github.com/saurabhsharma2u/search-console-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/saurabhsharma2u/search-console-mcp/actions/workflows/ci.yml)
15
12
 
@@ -28,7 +25,6 @@ A Model Context Protocol (MCP) server that transforms how you interact with Goog
28
25
  > "Find low-hanging fruit keywords (positions 11-20) with high impressions that I should optimize."
29
26
 
30
27
  ---
31
-
32
28
  ## šŸŽÆ Magic Prompts
33
29
 
34
30
  Copy and paste these into your MCP client (Claude Desktop, etc.) to see the intelligence engine in action:
@@ -53,69 +49,62 @@ Copy and paste these into your MCP client (Claude Desktop, etc.) to see the inte
53
49
 
54
50
  ---
55
51
 
56
- ## šŸ”’ Security & Privacy
52
+ ## šŸ” Authentication (Desktop Flow)
53
+
54
+ Search Console MCP uses a **Secure Desktop Flow**. This provides high-security, professional grade authentication for your Google account:
55
+ - **Multi-Account Support**: Automatically detects and stores separate tokens for different Google accounts based on your email.
56
+ - **System Keychain Primary**: Tokens are stored in your OS's native credential manager (macOS Keychain, Windows Credential Manager, or Linux Secret Service).
57
+ - **AES-256-GCM Hardware-Bound Encryption**: Fallback storage is encrypted with AES-256-GCM using a key derived from your unique hardware machine ID. Tokens stolen from your machine cannot be decrypted on another computer.
58
+ - **Silent Background Refresh**: Tokens auto-refresh silently when they expire.
59
+
60
+ ### šŸš€ Step 1 — Initiate Login
61
+ Run the following command to start the authorization process:
62
+ ```bash
63
+ npx search-console-mcp setup
64
+ ```
65
+
66
+ The CLI will:
67
+ 1. Briefly start a secure local server to handle the redirect.
68
+ 2. Open your default web browser to the Google Authorization page.
69
+ 3. Automatically fetch your email after authorization to label your credentials securely.
57
70
 
58
- We take your data security seriously. This tool is designed to be **local-first** and **secure by default**.
71
+ ### šŸ”‘ Step 2 — Logout & Management
72
+ To wipe your credentials from both the keychain and the disk:
73
+ ```bash
74
+ # Logout of the default account
75
+ npx search-console-mcp logout
59
76
 
60
- * **Credentials Never Logged**: Your Google Service Account keys are used *only* in memory to authenticate with Google APIs. They are never written to disk, logs, or sent to any third-party server.
61
- * **Local Execution**: The code runs entirely on your local machine (or wherever you host your MCP server).
62
- * **Path Traversal Protection**: The setup wizard implements strict validation for file paths, including null-byte removal, extension enforcement (.json), and size limits (1MB) to prevent unauthorized file access.
63
- * **Direct Communication**: All API calls go directly from your machine to Google's servers (`googleapis.com`). There is no middleman or proxy server.
64
- * **Open Source**: The code is fully open source. You can audit exactly how your credentials are used in `src/google-client.ts`.
77
+ # Logout of a specific account
78
+ npx search-console-mcp logout user@gmail.com
79
+ ```
65
80
 
66
81
  ---
67
82
 
68
- ## Features
83
+ ## šŸ”‘ Alternative: Service Account (Advanced)
69
84
 
70
- - **Advanced Analytics**: Query performance data with powerful filters (Regex supported).
71
- - *New*: Support for 'News', 'Discover', 'Image' search types.
72
- - *New*: 'Fresh' data support for real-time monitoring.
73
- - *New*: **Drop Attribution** to identify device-level traffic losses.
74
- - *New*: **Time Series Insights** with rolling averages and trend forecasting.
75
- - **Trend Detection**: Automatically identify rising or falling trends in your traffic.
76
- - **Anomaly Detection**: Spot unusual spikes or drops that need attention.
77
- - **Sitemaps Management**: List, submit, and validte sitemaps.
78
- - **URL Inspection**: Check real-time indexing status and mobile usability.
79
- - **PageSpeed Insights**: Integrated performance and Core Web Vitals analysis.
80
- - **Schema Validation**: Validate JSON-LD structured data.
85
+ For server-side environments or automated tasks where interactive login isn't possible, you can use a Google Cloud Service Account.
86
+
87
+ ### Setup:
88
+ 1. **Create Service Account**: Go to the [Google Cloud Console](https://console.cloud.google.com/iam-admin/serviceaccounts) and create a service account.
89
+ 2. **Generate Key**: Click "Keys" > "Add Key" > "Create new key" (JSON). Download this file.
90
+ 3. **Share Access**: In Google Search Console, add the service account's email address (e.g., `account@project.iam.gserviceaccount.com`) as a user with at least "Full" or "Restricted" permissions.
91
+ 4. **Configure**: Point the server to your key file:
92
+ ```bash
93
+ export GOOGLE_APPLICATION_CREDENTIALS="/path/to/your/key.json"
94
+ ```
81
95
 
82
96
  ---
83
97
 
84
- ## Quick Start
85
98
 
86
- ### 1. Run the Setup Wizard
87
- First, use the interactive wizard to validate your credentials and get your configuration:
99
+ ## šŸ›”ļø Fort Knox Security
88
100
 
89
- ```bash
90
- npx search-console-mcp-setup
91
- ```
101
+ This MCP server implements a multi-layered security architecture:
92
102
 
93
- The wizard will guide you through:
94
- - Creating a Google Service Account.
95
- - Adding the account to Search Console.
96
- - Validating your JSON key file.
97
- - Generating the configuration for your MCP client.
98
-
99
- ### 2. Connect to an MCP Client
100
-
101
- **Note:** The server uses `stdio` to communicate. Do not run `npx search-console-mcp` directly in your terminal; it is meant to be run by an MCP client like Claude Desktop or Cursor.
102
-
103
- #### Claude Desktop Configuration
104
- Add this to your `claude_desktop_config.json`:
105
-
106
- ```json
107
- {
108
- "mcpServers": {
109
- "google-search-console": {
110
- "command": "npx",
111
- "args": ["-y", "search-console-mcp"],
112
- "env": {
113
- "GOOGLE_APPLICATION_CREDENTIALS": "/absolute/path/to/your/service-account-key.json"
114
- }
115
- }
116
- }
117
- }
118
- ```
103
+ * **Keychain Integration**: Primarily uses the **macOS Keychain**, **Windows Credential Manager**, or **libsecret (Linux)** to store tokens.
104
+ * **Hardware-Bound Vault**: Fallback tokens are stored in `~/.search-console-mcp-tokens.enc` and encrypted with **AES-256-GCM**.
105
+ * **Machine Fingerprinting**: The encryption key is derived from your unique hardware UUID and OS user. The encrypted file is useless if moved to another machine.
106
+ * **Minimalist Storage**: Only the `refresh_token` and `expiry_date` are stored.
107
+ * **Strict Unix Permissions**: The fallback file is created with `mode 600` (read/write only by your user).
119
108
 
120
109
  ---
121
110
 
@@ -155,6 +144,7 @@ These are low-level tools designed to be used by other AI agents to build comple
155
144
  |------|-------------|
156
145
  | `sites_list` | List all verified sites. |
157
146
  | `sites_add` / `sites_delete` | Manage properties. |
147
+ | `sites_health_check` | **[NEW]** Run a health check on one or all sites. Checks WoW performance, sitemaps, and anomalies. |
158
148
  | `sitemaps_list` / `sitemaps_submit` | Manage sitemaps. |
159
149
 
160
150
  ### Inspection & Validation
@@ -164,12 +154,9 @@ These are low-level tools designed to be used by other AI agents to build comple
164
154
  | `pagespeed_analyze` | Lighthouse scores & Core Web Vitals. |
165
155
  | `schema_validate` | Validate Structured Data (JSON-LD). |
166
156
 
167
- ---
168
-
169
- ## Contributing
170
157
 
171
- We love contributions! Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for details.
172
158
 
173
159
  ## License
174
160
 
175
- MIT
161
+ [MIT](LICENSE.md)
162
+ [Contributing](CONTRIBUTING.md)
@@ -1,18 +1,59 @@
1
1
  export const algorithmUpdatesDocs = `
2
2
  # Google Algorithm Updates Reference
3
3
 
4
- The \`analytics_drop_attribution\` tool correlates traffic drops with major known Google Algorithm Updates. The following updates are currently supported:
4
+ The \`analytics_drop_attribution\` tool correlates traffic drops with major known Google Algorithm Updates. The following updates are currently tracked (source: [Search Engine Journal](https://www.searchenginejournal.com/google-algorithm-history/)):
5
+
6
+ ## 2026
7
+
8
+ | Date | Update Name | Impact Area |
9
+ |------|-------------|-------------|
10
+ | 2026-02-05 | February 2026 Discover Core Update | Discover Feed |
11
+
12
+ ## 2025
13
+
14
+ | Date | Update Name | Impact Area |
15
+ |------|-------------|-------------|
16
+ | 2025-12-11 | December 2025 Core Update | General Ranking |
17
+ | 2025-08-26 | August 2025 Spam Update | Spam / Content Quality |
18
+ | 2025-06-30 | June 2025 Core Update | General Ranking |
19
+ | 2025-03-13 | March 2025 Core Update | General Ranking |
20
+
21
+ ## 2024
5
22
 
6
23
  | Date | Update Name | Impact Area |
7
24
  |------|-------------|-------------|
8
- | 2025-01-15 | January 2025 Core Update | General Ranking |
25
+ | 2024-12-19 | December 2024 Spam Update | Spam / Content Quality |
26
+ | 2024-12-12 | December 2024 Core Update | General Ranking |
9
27
  | 2024-11-11 | November 2024 Core Update | General Ranking |
10
- | 2024-08-15 | August 2024 Core Update | General Ranking |
28
+ | 2024-08-15 | August 2024 Core Update | General Ranking / Quality |
11
29
  | 2024-06-20 | June 2024 Spam Update | Content Quality / Spam |
30
+ | 2024-05-14 | AI Overviews Rollout | Search Features / SERP |
31
+ | 2024-05-06 | Site Reputation Abuse (Manual Actions) | Parasite SEO / Spam |
12
32
  | 2024-03-05 | March 2024 Core Update | General Ranking / Quality |
33
+
34
+ ## 2023
35
+
36
+ | Date | Update Name | Impact Area |
37
+ |------|-------------|-------------|
38
+ | 2023-11-08 | November 2023 Reviews Update | Review Content |
13
39
  | 2023-11-02 | November 2023 Core Update | General Ranking |
14
40
  | 2023-10-05 | October 2023 Core Update | General Ranking |
41
+ | 2023-10-04 | October 2023 Spam Update | Spam / Multi-language |
15
42
  | 2023-09-14 | September 2023 Helpful Content Update | Quality / Value |
43
+ | 2023-08-22 | August 2023 Core Update | General Ranking |
44
+ | 2023-04-12 | April 2023 Reviews Update | Review Content |
45
+ | 2023-03-15 | March 2023 Core Update | General Ranking |
46
+ | 2023-02-21 | February 2023 Product Reviews Update | Product Reviews |
47
+
48
+ ## 2022
49
+
50
+ | Date | Update Name | Impact Area |
51
+ |------|-------------|-------------|
52
+ | 2022-12-14 | December 2022 Link Spam Update | Link Spam / SpamBrain |
53
+ | 2022-12-05 | December 2022 Helpful Content Update | Quality / Value |
54
+ | 2022-10-19 | October 2022 Spam Update | Spam |
55
+ | 2022-09-20 | September 2022 Product Review Update | Product Reviews |
56
+ | 2022-09-12 | September 2022 Core Update | General Ranking |
16
57
 
17
58
  ## How Attribution Works
18
59
  When a traffic drop is detected, the system checks if it occurred within **+/- 2 days** of any of these dates. If it matches, the update is flagged as a potential primary cause of the drop.
@@ -1,16 +1,76 @@
1
1
  import { google } from 'googleapis';
2
- const SCOPES = ['https://www.googleapis.com/auth/webmasters'];
2
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { homedir } from 'os';
5
+ import { createCipheriv, createDecipheriv, scryptSync, randomBytes } from 'crypto';
6
+ import nodeMachineId from 'node-machine-id';
7
+ const { machineIdSync } = nodeMachineId;
8
+ const SCOPES = [
9
+ 'https://www.googleapis.com/auth/webmasters.readonly',
10
+ 'https://www.googleapis.com/auth/userinfo.email'
11
+ ];
12
+ const TOKEN_PATH = join(homedir(), '.search-console-mcp-tokens.enc');
13
+ const SERVICE_NAME = 'io.github.saurabhsharma2u.search-console-mcp';
14
+ const DEFAULT_ACCOUNT = 'default';
15
+ // Default Client ID for Desktop Flow
16
+ export const DEFAULT_CLIENT_ID = process.env.GOOGLE_CLIENT_ID || '347626597503-dr6t24m0i3g1nl1suam86rs650t3fhau.apps.googleusercontent.com';
17
+ export const DEFAULT_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET || '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';
18
+ const ENCRYPTION_ALGORITHM = 'aes-256-gcm';
19
+ function getEncryptionKey() {
20
+ const mId = machineIdSync();
21
+ const salt = process.env.USER || 'sc-mcp-salt';
22
+ return scryptSync(mId, salt, 32);
23
+ }
24
+ function encrypt(text) {
25
+ const iv = randomBytes(12);
26
+ const key = getEncryptionKey();
27
+ const cipher = createCipheriv(ENCRYPTION_ALGORITHM, key, iv);
28
+ let encrypted = cipher.update(text, 'utf8', 'hex');
29
+ encrypted += cipher.final('hex');
30
+ const authTag = cipher.getAuthTag().toString('hex');
31
+ return `${iv.toString('hex')}:${authTag}:${encrypted}`;
32
+ }
33
+ function decrypt(data) {
34
+ const [ivHex, authTagHex, encryptedHex] = data.split(':');
35
+ const iv = Buffer.from(ivHex, 'hex');
36
+ const authTag = Buffer.from(authTagHex, 'hex');
37
+ const key = getEncryptionKey();
38
+ const decipher = createDecipheriv(ENCRYPTION_ALGORITHM, key, iv);
39
+ decipher.setAuthTag(authTag);
40
+ let decrypted = decipher.update(encryptedHex, 'hex', 'utf8');
41
+ decrypted += decipher.final('utf8');
42
+ return decrypted;
43
+ }
3
44
  let cachedClient = null;
4
- /**
5
- * Get an authenticated Google Search Console API client.
6
- * Supports both file-based credentials (GOOGLE_APPLICATION_CREDENTIALS)
7
- * and environment variable credentials (GOOGLE_CLIENT_EMAIL + GOOGLE_PRIVATE_KEY).
8
- */
9
- export async function getSearchConsoleClient() {
10
- if (cachedClient) {
45
+ export async function getSearchConsoleClient(targetEmail) {
46
+ if (cachedClient && !targetEmail) {
11
47
  return cachedClient;
12
48
  }
13
- // Option 1: Environment variables (for serverless/Cloudflare)
49
+ // 1. Load Tokens (Keychain first, then File)
50
+ const tokens = await loadTokens(targetEmail);
51
+ if (tokens) {
52
+ try {
53
+ const oauth2Client = new google.auth.OAuth2(process.env.GOOGLE_CLIENT_ID || DEFAULT_CLIENT_ID, process.env.GOOGLE_CLIENT_SECRET || DEFAULT_CLIENT_SECRET);
54
+ oauth2Client.setCredentials(tokens);
55
+ // Check for expiry (refresh if needed)
56
+ if (tokens.expiry_date && tokens.expiry_date <= Date.now()) {
57
+ const { credentials } = await oauth2Client.refreshAccessToken();
58
+ // Since we refresh, we need the email to save back.
59
+ // If we don't have targetEmail, we try to fetch it from the new credentials or previous data
60
+ const email = targetEmail || await getUserEmail(credentials);
61
+ await saveTokens(credentials, email);
62
+ oauth2Client.setCredentials(credentials);
63
+ }
64
+ const client = google.searchconsole({ version: 'v1', auth: oauth2Client });
65
+ if (!targetEmail)
66
+ cachedClient = client;
67
+ return client;
68
+ }
69
+ catch (error) {
70
+ console.error('Failed to use stored OAuth2 tokens:', error.message);
71
+ }
72
+ }
73
+ // 2. Fallback to Service Account (Environment Variables)
14
74
  if (!process.env.GOOGLE_APPLICATION_CREDENTIALS && process.env.GOOGLE_CLIENT_EMAIL && process.env.GOOGLE_PRIVATE_KEY) {
15
75
  const jwtClient = new google.auth.JWT({
16
76
  email: process.env.GOOGLE_CLIENT_EMAIL,
@@ -21,11 +81,169 @@ export async function getSearchConsoleClient() {
21
81
  cachedClient = google.searchconsole({ version: 'v1', auth: jwtClient });
22
82
  return cachedClient;
23
83
  }
24
- // Option 2: File-based credentials (default)
84
+ // 3. Fallback to File-based credentials
25
85
  const auth = new google.auth.GoogleAuth({
26
86
  scopes: SCOPES,
27
87
  });
28
- const client = await auth.getClient();
29
- cachedClient = google.searchconsole({ version: 'v1', auth: client });
30
- return cachedClient;
88
+ try {
89
+ const client = await auth.getClient();
90
+ cachedClient = google.searchconsole({ version: 'v1', auth: client });
91
+ return cachedClient;
92
+ }
93
+ catch (error) {
94
+ throw new Error('No valid authentication found. Please run "search-console-mcp login" to authorize.');
95
+ }
96
+ }
97
+ export async function getUserEmail(tokens) {
98
+ const oauth2Client = new google.auth.OAuth2(process.env.GOOGLE_CLIENT_ID || DEFAULT_CLIENT_ID, process.env.GOOGLE_CLIENT_SECRET || DEFAULT_CLIENT_SECRET);
99
+ oauth2Client.setCredentials(tokens);
100
+ const oauth2 = google.oauth2({ version: 'v2', auth: oauth2Client });
101
+ const userInfo = await oauth2.userinfo.get();
102
+ return userInfo.data.email || DEFAULT_ACCOUNT;
103
+ }
104
+ export async function logout(email) {
105
+ const target = email || DEFAULT_ACCOUNT;
106
+ // 1. Try Keychain
107
+ try {
108
+ const { Entry } = await import('@napi-rs/keyring');
109
+ const entry = new Entry(SERVICE_NAME, target);
110
+ await entry.deletePassword();
111
+ }
112
+ catch (e) { }
113
+ // 2. Clear from file
114
+ if (existsSync(TOKEN_PATH)) {
115
+ try {
116
+ const encryptedData = readFileSync(TOKEN_PATH, 'utf-8');
117
+ const decrypted = decrypt(encryptedData);
118
+ const allTokens = JSON.parse(decrypted);
119
+ delete allTokens[target];
120
+ if (Object.keys(allTokens).length === 0) {
121
+ // Just delete the file if no accounts left
122
+ import('fs').then(fs => fs.unlinkSync(TOKEN_PATH)).catch(() => { });
123
+ }
124
+ else {
125
+ const updatedEncrypted = encrypt(JSON.stringify(allTokens));
126
+ writeFileSync(TOKEN_PATH, updatedEncrypted, { mode: 0o600 });
127
+ }
128
+ }
129
+ catch (e) { }
130
+ }
131
+ }
132
+ export async function loadTokens(email) {
133
+ const target = email || DEFAULT_ACCOUNT;
134
+ // 1. Try Keychain
135
+ try {
136
+ const { Entry } = await import('@napi-rs/keyring');
137
+ const entry = new Entry(SERVICE_NAME, target);
138
+ const secret = await entry.getPassword();
139
+ if (secret) {
140
+ return JSON.parse(secret);
141
+ }
142
+ }
143
+ catch (e) { }
144
+ // 2. Try Encrypted File
145
+ if (existsSync(TOKEN_PATH)) {
146
+ try {
147
+ const encryptedData = readFileSync(TOKEN_PATH, 'utf-8');
148
+ const decrypted = decrypt(encryptedData);
149
+ const allTokens = JSON.parse(decrypted);
150
+ return allTokens[target] || (email ? null : Object.values(allTokens)[0]);
151
+ }
152
+ catch (e) { }
153
+ }
154
+ return null;
155
+ }
156
+ export async function saveTokens(tokens, email) {
157
+ const target = email || DEFAULT_ACCOUNT;
158
+ // Only store what we need
159
+ const minimalTokens = {
160
+ refresh_token: tokens.refresh_token,
161
+ expiry_date: tokens.expiry_date,
162
+ access_token: tokens.access_token // Needed for immediate use, but refresh_token is the key
163
+ };
164
+ const tokenStr = JSON.stringify(minimalTokens);
165
+ // 1. Try Keychain
166
+ let keychainSuccess = false;
167
+ try {
168
+ const { Entry } = await import('@napi-rs/keyring');
169
+ const entry = new Entry(SERVICE_NAME, target);
170
+ await entry.setPassword(tokenStr);
171
+ keychainSuccess = true;
172
+ }
173
+ catch (e) { }
174
+ // 2. Always write to encrypted file as a fallback
175
+ try {
176
+ let allTokens = {};
177
+ if (existsSync(TOKEN_PATH)) {
178
+ try {
179
+ const encryptedData = readFileSync(TOKEN_PATH, 'utf-8');
180
+ const decrypted = decrypt(encryptedData);
181
+ allTokens = JSON.parse(decrypted);
182
+ }
183
+ catch (e) { }
184
+ }
185
+ allTokens[target] = minimalTokens;
186
+ const encrypted = encrypt(JSON.stringify(allTokens));
187
+ writeFileSync(TOKEN_PATH, encrypted, { mode: 0o600 });
188
+ }
189
+ catch (e) {
190
+ if (!keychainSuccess)
191
+ throw e;
192
+ }
193
+ }
194
+ export async function initiateDeviceFlow(clientId) {
195
+ const response = await fetch('https://oauth2.googleapis.com/device/code', {
196
+ method: 'POST',
197
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
198
+ body: new URLSearchParams({
199
+ client_id: clientId,
200
+ scope: SCOPES.join(' ')
201
+ })
202
+ });
203
+ if (!response.ok) {
204
+ const error = await response.text();
205
+ throw new Error(`Failed to initiate device flow: ${error}`);
206
+ }
207
+ return await response.json();
208
+ }
209
+ export async function pollForTokens(clientId, clientSecret, deviceCode, interval) {
210
+ // This is now deprecated as Device Flow doesn't support Search Console scopes
211
+ throw new Error("Device Flow is not supported for Search Console API.");
212
+ }
213
+ export async function startLocalFlow(clientId, clientSecret) {
214
+ const { createServer } = await import('http');
215
+ const { google } = await import('googleapis');
216
+ const open = (await import('open')).default;
217
+ const REDIRECT_URI = 'http://localhost:3000/oauth2callback';
218
+ const oauth2Client = new google.auth.OAuth2(clientId, clientSecret, REDIRECT_URI);
219
+ const authUrl = oauth2Client.generateAuthUrl({
220
+ access_type: 'offline',
221
+ scope: SCOPES,
222
+ prompt: 'consent'
223
+ });
224
+ return new Promise((resolve, reject) => {
225
+ const server = createServer(async (req, res) => {
226
+ try {
227
+ if (req.url?.startsWith('/oauth2callback')) {
228
+ const url = new URL(req.url, `http://${req.headers.host}`);
229
+ const code = url.searchParams.get('code');
230
+ if (code) {
231
+ const { tokens } = await oauth2Client.getToken(code);
232
+ res.writeHead(200, { 'Content-Type': 'text/html' });
233
+ res.end('<h1>Authentication Successful!</h1><p>You can close this tab now and return to your terminal.</p>');
234
+ server.close();
235
+ resolve(tokens);
236
+ }
237
+ }
238
+ }
239
+ catch (e) {
240
+ res.writeHead(500);
241
+ res.end('<h1>Authentication Failed</h1>');
242
+ server.close();
243
+ reject(e);
244
+ }
245
+ }).listen(3000);
246
+ console.log('\nOpening your browser to authorize Search Console access...');
247
+ open(authUrl);
248
+ });
31
249
  }
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import 'dotenv/config';
2
3
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
5
  import { z } from "zod";
@@ -11,7 +12,11 @@ import * as seoInsights from "./tools/seo-insights.js";
11
12
  import * as seoPrimitives from "./tools/seo-primitives.js";
12
13
  import * as schemaValidator from "./tools/schema-validator.js";
13
14
  import * as advancedAnalytics from "./tools/advanced-analytics.js";
15
+ import * as sitesHealth from "./tools/sites-health.js";
14
16
  import { formatError } from "./errors.js";
17
+ import { existsSync } from "fs";
18
+ import { join } from "path";
19
+ import { homedir } from "os";
15
20
  const server = new McpServer({
16
21
  name: "search-console-mcp",
17
22
  version: "1.0.0",
@@ -61,6 +66,19 @@ server.tool("sites_get", "Get information about a specific site", { siteUrl: z.s
61
66
  return formatError(error);
62
67
  }
63
68
  });
69
+ server.tool("sites_health_check", "Run a health check on one or all verified sites. Checks week-over-week performance, sitemap status, and traffic anomalies. Returns a structured report with an overall status (healthy/warning/critical) per site.", {
70
+ siteUrl: z.string().optional().describe("Optional. The URL of a specific site to check. If omitted, checks all verified sites.")
71
+ }, async ({ siteUrl }) => {
72
+ try {
73
+ const result = await sitesHealth.healthCheck(siteUrl);
74
+ return {
75
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
76
+ };
77
+ }
78
+ catch (error) {
79
+ return formatError(error);
80
+ }
81
+ });
64
82
  // Sitemaps Tools
65
83
  server.tool("sitemaps_list", "List sitemaps for a site", { siteUrl: z.string().describe("The URL of the site") }, async ({ siteUrl }) => {
66
84
  try {
@@ -740,17 +758,67 @@ Provide a summary with actionable recommendations.`
740
758
  }
741
759
  }]
742
760
  }));
761
+ server.prompt("site-health-check", {
762
+ siteUrl: z.string().optional().describe("Optional. The URL of a specific site to check. If omitted, checks all verified sites.")
763
+ }, ({ siteUrl }) => ({
764
+ messages: [{
765
+ role: "user",
766
+ content: {
767
+ type: "text",
768
+ text: `Run a comprehensive health check${siteUrl ? ` for ${siteUrl}` : ' across all my verified sites'}.
769
+
770
+ Use the 'sites_health_check' tool${siteUrl ? ` with siteUrl '${siteUrl}'` : ' without a siteUrl to check all sites'}.
771
+
772
+ Then for each site in the results:
773
+
774
+ 1. **Summarize the status** (healthy / warning / critical) with a clear visual indicator.
775
+ 2. **Performance:** Report the week-over-week changes in clicks, impressions, CTR, and position.
776
+ 3. **Sitemaps:** Note any missing sitemaps, errors, or warnings.
777
+ 4. **Anomalies:** Highlight any traffic anomaly drops detected.
778
+
779
+ If any site has a 'critical' or 'warning' status:
780
+ - List the specific issues found.
781
+ - For critical traffic drops, use 'analytics_drop_attribution' to check if a Google algorithm update is the likely cause.
782
+ - For sitemap errors, use 'sitemaps_list' to get detailed error information.
783
+ - Provide 3 prioritized action items for each affected site.
784
+
785
+ End with an overall portfolio summary and the single most important action to take right now.`
786
+ }
787
+ }]
788
+ }));
743
789
  async function main() {
744
- if (!process.env.GOOGLE_APPLICATION_CREDENTIALS) {
790
+ const command = process.argv[2];
791
+ // Handle standalone commands
792
+ if (command === 'setup') {
793
+ const { main: setupMain } = await import('./setup.js');
794
+ await setupMain();
795
+ return;
796
+ }
797
+ if (command === 'logout') {
798
+ const { runLogout } = await import('./setup.js');
799
+ await runLogout();
800
+ return;
801
+ }
802
+ if (command === 'login') {
803
+ const { login } = await import('./setup.js');
804
+ await login();
805
+ return;
806
+ }
807
+ // Check for credentials
808
+ const hasServiceAccount = !!process.env.GOOGLE_APPLICATION_CREDENTIALS || (!!process.env.GOOGLE_CLIENT_EMAIL && !!process.env.GOOGLE_PRIVATE_KEY);
809
+ const tokenPath = join(homedir(), '.search-console-mcp-tokens.enc');
810
+ const hasOAuthTokens = existsSync(tokenPath);
811
+ if (!hasServiceAccount && !hasOAuthTokens) {
745
812
  console.error('\n╔══════════════════════════════════════════════════════════════╗');
746
813
  console.error('ā•‘ šŸš€ Google Search Console MCP Server ā•‘');
747
814
  console.error('ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•\n');
748
- console.error('āŒ GOOGLE_APPLICATION_CREDENTIALS environment variable is not set.\n');
749
- console.error('šŸ’” To set up the server, run the setup wizard:');
750
- console.error(' npx search-console-mcp-setup\n');
751
- console.error('Alternatively, set the variable manually:');
752
- console.error(' export GOOGLE_APPLICATION_CREDENTIALS=/path/to/your/key.json\n');
815
+ console.error('āŒ No authentication found.\n');
816
+ console.error('šŸ’” Please authorize with your Google Account:');
817
+ console.error(' npx -y search-console-mcp login\n');
818
+ console.error('Alternatively, run the setup wizard for other options:');
819
+ console.error(' npx -y search-console-mcp setup\n');
753
820
  console.error('─'.repeat(64) + '\n');
821
+ // We don't exit here because the transport might still be needed or the user might be piping output
754
822
  }
755
823
  const transport = new StdioServerTransport();
756
824
  await server.connect(transport);