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 +48 -61
- package/dist/docs/algorithm-updates.js +44 -3
- package/dist/google-client.js +231 -13
- package/dist/index.js +74 -6
- package/dist/setup.js +125 -96
- package/dist/tools/advanced-analytics.js +25 -1
- package/dist/tools/analytics.js +78 -27
- package/dist/tools/schema-validator.js +1 -1
- package/dist/tools/seo-insights.js +6 -5
- package/dist/tools/seo-primitives.js +7 -16
- package/dist/tools/sites-health.js +157 -0
- package/dist/utils/regex.js +41 -0
- package/package.json +7 -3
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
|
[](https://opensource.org/licenses/MIT)
|
|
14
11
|
[](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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
##
|
|
83
|
+
## š Alternative: Service Account (Advanced)
|
|
69
84
|
|
|
70
|
-
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
87
|
-
First, use the interactive wizard to validate your credentials and get your configuration:
|
|
99
|
+
## š”ļø Fort Knox Security
|
|
88
100
|
|
|
89
|
-
|
|
90
|
-
npx search-console-mcp-setup
|
|
91
|
-
```
|
|
101
|
+
This MCP server implements a multi-layered security architecture:
|
|
92
102
|
|
|
93
|
-
|
|
94
|
-
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
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
|
-
|
|
|
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.
|
package/dist/google-client.js
CHANGED
|
@@ -1,16 +1,76 @@
|
|
|
1
1
|
import { google } from 'googleapis';
|
|
2
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
84
|
+
// 3. Fallback to File-based credentials
|
|
25
85
|
const auth = new google.auth.GoogleAuth({
|
|
26
86
|
scopes: SCOPES,
|
|
27
87
|
});
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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('ā
|
|
749
|
-
console.error('š”
|
|
750
|
-
console.error(' npx search-console-mcp
|
|
751
|
-
console.error('Alternatively,
|
|
752
|
-
console.error('
|
|
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);
|