webmaster-mcp 0.1.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/LICENSE +21 -0
- package/README.md +238 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +49 -0
- package/dist/core/analysis/index.d.ts +43 -0
- package/dist/core/analysis/index.js +14 -0
- package/dist/core/auth/accounts.d.ts +11 -0
- package/dist/core/auth/accounts.js +12 -0
- package/dist/core/auth/prompt.d.ts +1 -0
- package/dist/core/auth/prompt.js +4 -0
- package/dist/core/auth/store.d.ts +11 -0
- package/dist/core/auth/store.js +22 -0
- package/dist/core/cache/index.d.ts +11 -0
- package/dist/core/cache/index.js +21 -0
- package/dist/core/errors.d.ts +9 -0
- package/dist/core/errors.js +24 -0
- package/dist/core/normalize/index.d.ts +4 -0
- package/dist/core/normalize/index.js +15 -0
- package/dist/core/scheduler.d.ts +18 -0
- package/dist/core/scheduler.js +52 -0
- package/dist/core/service.d.ts +30 -0
- package/dist/core/service.js +48 -0
- package/dist/core/types/index.d.ts +73 -0
- package/dist/core/types/index.js +1 -0
- package/dist/core/validation.d.ts +18 -0
- package/dist/core/validation.js +35 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +4 -0
- package/dist/mcp/tools/index.d.ts +2 -0
- package/dist/mcp/tools/index.js +15 -0
- package/dist/providers/bing/index.d.ts +20 -0
- package/dist/providers/bing/index.js +31 -0
- package/dist/providers/google/auth.d.ts +10 -0
- package/dist/providers/google/auth.js +57 -0
- package/dist/providers/google/index.d.ts +22 -0
- package/dist/providers/google/index.js +35 -0
- package/dist/providers/yandex/auth.d.ts +5 -0
- package/dist/providers/yandex/auth.js +9 -0
- package/dist/providers/yandex/index.d.ts +22 -0
- package/dist/providers/yandex/index.js +70 -0
- package/dist/server/context.d.ts +10 -0
- package/dist/server/context.js +10 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 webmaster-mcp contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# webmaster-mcp
|
|
2
|
+
|
|
3
|
+
Local-first MCP server for Google Search Console, Bing Webmaster Tools, and Yandex Webmaster. One normalized tool surface, local credential storage, SQLite response cache, and a shared background process through mcponce.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Node.js 20 or newer
|
|
8
|
+
- An OS credential store (macOS Keychain, Windows Credential Manager, or Linux Secret Service)
|
|
9
|
+
- Provider accounts with access to the sites you want to inspect
|
|
10
|
+
|
|
11
|
+
Install and run after publication:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx webmaster-mcp auth google
|
|
15
|
+
npx webmaster-mcp auth bing
|
|
16
|
+
npx webmaster-mcp auth yandex
|
|
17
|
+
npx webmaster-mcp auth status
|
|
18
|
+
npx webmaster-mcp sites
|
|
19
|
+
npx webmaster-mcp mcp
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
For a local checkout:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install
|
|
26
|
+
npm run build
|
|
27
|
+
node dist/cli/index.js auth status
|
|
28
|
+
node dist/cli/index.js mcp
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### MCP Client Configuration
|
|
32
|
+
|
|
33
|
+
Add `webmaster-mcp` to your preferred AI environment's configuration file:
|
|
34
|
+
|
|
35
|
+
#### Google Antigravity
|
|
36
|
+
Add to `~/.gemini/config/mcp_config.json` (global) or `.agents/mcp_config.json` (workspace):
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"mcpServers": {
|
|
40
|
+
"webmaster": {
|
|
41
|
+
"command": "npx",
|
|
42
|
+
"args": ["-y", "webmaster-mcp", "mcp"]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
#### Cursor
|
|
49
|
+
Add to `~/.cursor/mcp.json` or `.cursor/mcp.json`:
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"mcpServers": {
|
|
53
|
+
"webmaster": {
|
|
54
|
+
"command": "npx",
|
|
55
|
+
"args": ["-y", "webmaster-mcp", "mcp"]
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
#### Claude Desktop
|
|
62
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"mcpServers": {
|
|
66
|
+
"webmaster": {
|
|
67
|
+
"command": "npx",
|
|
68
|
+
"args": ["-y", "webmaster-mcp", "mcp"]
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
#### OpenAI Codex CLI
|
|
75
|
+
Add via CLI or edit `~/.codex/config.json`:
|
|
76
|
+
```bash
|
|
77
|
+
codex mcp add webmaster -- npx -y webmaster-mcp mcp
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`mcponce` automatically starts and reuses one local background instance, so multiple clients safely share the SQLite response cache, rate limits, and authentication state.
|
|
81
|
+
|
|
82
|
+
## Authentication
|
|
83
|
+
|
|
84
|
+
### Google
|
|
85
|
+
|
|
86
|
+
#### Setting Up Google Cloud OAuth 2.0 (Step-by-Step)
|
|
87
|
+
|
|
88
|
+
1. **Create a Google Cloud Project:**
|
|
89
|
+
- Go to the [Google Cloud Console](https://console.cloud.google.com/).
|
|
90
|
+
- Click the project dropdown in the top bar and select **New Project**.
|
|
91
|
+
- Enter a project name (e.g., `Webmaster-MCP`) and click **Create**.
|
|
92
|
+
- Select your newly created project from the top dropdown.
|
|
93
|
+
|
|
94
|
+
2. **Enable Google Search Console API:**
|
|
95
|
+
- Navigate to **APIs & Services** > **Library** in the left navigation menu.
|
|
96
|
+
- Search for **Google Search Console API**.
|
|
97
|
+
- Click on it and select **Enable**.
|
|
98
|
+
|
|
99
|
+
3. **Configure OAuth Consent Screen:**
|
|
100
|
+
- Go to **APIs & Services** > **OAuth consent screen**.
|
|
101
|
+
- **User Type Selection:**
|
|
102
|
+
- **Recommended for Google Workspace / Organization accounts:** Choose **Internal**. This skips app verification, requires no test user management, and grants immediate access to any user in your domain.
|
|
103
|
+
- **For personal `@gmail.com` accounts:** Choose **External** (Google only allows External for standard Gmail).
|
|
104
|
+
- Enter the required application details:
|
|
105
|
+
- **App name:** `Webmaster MCP`
|
|
106
|
+
- **User support email:** Your email address.
|
|
107
|
+
- **Developer contact information:** Your email address.
|
|
108
|
+
- Click **Save and Continue**.
|
|
109
|
+
- In the **Scopes** step, click **Add or Remove Scopes**, search for `webmasters`, select `.../auth/webmasters.readonly` (and optionally `.../auth/webmasters` if you need write scope), then click **Update** and **Save and Continue**.
|
|
110
|
+
- In the **Test users** step *(CRITICAL for External mode)*:
|
|
111
|
+
- Click **+ Add Users** and add your Google account email address.
|
|
112
|
+
- *(Note: If this step is omitted on External/Testing apps, Google blocks login with `Error 403: access_denied`).*
|
|
113
|
+
- Click **Save and Continue**.
|
|
114
|
+
|
|
115
|
+
4. **Create Desktop OAuth Client Credentials:**
|
|
116
|
+
- Go to **APIs & Services** > **Credentials**.
|
|
117
|
+
- Click **+ Create Credentials** at the top and select **OAuth client ID**.
|
|
118
|
+
- For **Application type**, choose **Desktop app** (*Desktop application types automatically support dynamic loopback redirect URIs like `http://127.0.0.1:<port>/callback` without static URI registration*).
|
|
119
|
+
- Set a name (e.g., `Webmaster CLI Client`) and click **Create**.
|
|
120
|
+
- Copy the generated **Client ID** and **Client Secret**.
|
|
121
|
+
|
|
122
|
+
5. **Set Environment Variables:**
|
|
123
|
+
- On macOS / Linux (`zsh` / `bash`):
|
|
124
|
+
```bash
|
|
125
|
+
export GOOGLE_CLIENT_ID="your-client-id.apps.googleusercontent.com"
|
|
126
|
+
export GOOGLE_CLIENT_SECRET="your-client-secret"
|
|
127
|
+
```
|
|
128
|
+
- On Windows (PowerShell):
|
|
129
|
+
```powershell
|
|
130
|
+
$env:GOOGLE_CLIENT_ID="your-client-id.apps.googleusercontent.com"
|
|
131
|
+
$env:GOOGLE_CLIENT_SECRET="your-client-secret"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
6. **Authorize via CLI:**
|
|
135
|
+
- Run:
|
|
136
|
+
```bash
|
|
137
|
+
npx webmaster-mcp auth google
|
|
138
|
+
# or locally:
|
|
139
|
+
node dist/cli/index.js auth google
|
|
140
|
+
```
|
|
141
|
+
- The CLI opens your default browser with a PKCE code challenge and starts a local loopback server.
|
|
142
|
+
- Log in with your authorized Google account. If Google displays "Google hasn't verified this app", click **Advanced** > **Go to Webmaster MCP (unsafe)**.
|
|
143
|
+
- Once authorized, the refresh token is securely stored in your OS keychain / credential store. The loopback server shuts down immediately. Tokens are refreshed automatically.
|
|
144
|
+
|
|
145
|
+
The default scope is `webmasters.readonly`. Run `auth google --write` only if you explicitly want the broader `webmasters` scope; this version has no write MCP tools.
|
|
146
|
+
|
|
147
|
+
### Bing
|
|
148
|
+
|
|
149
|
+
#### Generating a Bing Webmaster API Key (Step-by-Step)
|
|
150
|
+
|
|
151
|
+
1. Go to [Bing Webmaster Tools](https://www.bing.com/webmasters) and sign in with your Microsoft/Google account.
|
|
152
|
+
2. In the top-right corner, click the **Settings** gear icon (⚙️).
|
|
153
|
+
3. In the Settings menu, select **API Access** > **API Key**.
|
|
154
|
+
4. Click **Generate API Key** (or copy your existing API key).
|
|
155
|
+
5. Run:
|
|
156
|
+
```bash
|
|
157
|
+
npx webmaster-mcp auth bing
|
|
158
|
+
# or locally:
|
|
159
|
+
node dist/cli/index.js auth bing
|
|
160
|
+
```
|
|
161
|
+
6. Paste your API key into the hidden terminal prompt. The key is securely saved to your OS keychain / credential store.
|
|
162
|
+
|
|
163
|
+
Bing's JSON API transmits the key as an HTTPS query parameter as required by that API; this application does not log request URLs or include them in MCP errors.
|
|
164
|
+
|
|
165
|
+
### Yandex
|
|
166
|
+
|
|
167
|
+
#### Setting Up Yandex OAuth (Step-by-Step)
|
|
168
|
+
|
|
169
|
+
1. Go to the [Yandex OAuth App Console](https://oauth.yandex.com/client/new).
|
|
170
|
+
2. Enter an **App name** (e.g., `Webmaster MCP`).
|
|
171
|
+
3. Under **Platforms**, select **Web services** and set the callback URL or use the default verification code flow (`https://oauth.yandex.com/verification_code`).
|
|
172
|
+
4. Under **Data access (Permissions)**, search and add permissions for **Yandex Webmaster** (`webmaster:hostinfo` and `webmaster:verify`).
|
|
173
|
+
5. Save the application and copy your generated **Client ID**.
|
|
174
|
+
6. Set the environment variable:
|
|
175
|
+
```bash
|
|
176
|
+
export YANDEX_CLIENT_ID="your-yandex-client-id"
|
|
177
|
+
```
|
|
178
|
+
7. Run:
|
|
179
|
+
```bash
|
|
180
|
+
npx webmaster-mcp auth yandex
|
|
181
|
+
# or locally:
|
|
182
|
+
node dist/cli/index.js auth yandex
|
|
183
|
+
```
|
|
184
|
+
8. The CLI opens the Yandex authorization page in your browser. Copy the authorization token/code displayed on the page and paste it into the hidden terminal prompt. The token is securely stored in your OS keychain.
|
|
185
|
+
|
|
186
|
+
### Multi-Account & Credential Management
|
|
187
|
+
|
|
188
|
+
- **Named Accounts:** Add `--name personal` or `--name work` to any auth command to manage multiple accounts:
|
|
189
|
+
```bash
|
|
190
|
+
npx webmaster-mcp auth google --name agency
|
|
191
|
+
npx webmaster-mcp auth bing --name client1
|
|
192
|
+
```
|
|
193
|
+
- **Check Status:** View all stored accounts and their connection status:
|
|
194
|
+
```bash
|
|
195
|
+
npx webmaster-mcp auth status
|
|
196
|
+
```
|
|
197
|
+
- **Disconnect / Logout:** Remove a specific account:
|
|
198
|
+
```bash
|
|
199
|
+
npx webmaster-mcp auth logout google --name agency
|
|
200
|
+
```
|
|
201
|
+
- **MCP Tool Selection:** Tools accept an optional `account` parameter. If omitted, connected accounts are queried automatically and one account per engine is selected for each property (preferring `default`).
|
|
202
|
+
|
|
203
|
+
For development only, environment fallback values can be set as `WEBMASTER_GOOGLE_TOKEN`, `WEBMASTER_BING_TOKEN`, or `WEBMASTER_YANDEX_TOKEN` for the default account, or `WEBMASTER_GOOGLE_PERSONAL` and analogous names for a named account. Google environment access tokens cannot refresh. No credentials are stored in SQLite.
|
|
204
|
+
|
|
205
|
+
## Tools
|
|
206
|
+
|
|
207
|
+
`sites`, `performance`, `queries`, `pages`, `sitemaps`, `inspect_url`, `compare_engines`, `opportunities`, and `indexing_gaps`.
|
|
208
|
+
|
|
209
|
+
For example, call `performance` with:
|
|
210
|
+
|
|
211
|
+
```json
|
|
212
|
+
{"site":"https://example.com/","from":"2026-08-01","to":"2026-08-28","engines":["google","bing"]}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Tools return `results` plus sanitized `errors` for individual providers. Provider requests run concurrently. The analysis tools use deterministic thresholds and return the metrics supporting each finding. `indexing_gaps` accepts up to 20 `urls`; it reports **verified** differences only when both provider results have a known state. It also reports search visibility gaps, which mean only that a provider returned no performance row, not that a page is unindexed.
|
|
216
|
+
|
|
217
|
+
## Metric meaning and limits
|
|
218
|
+
|
|
219
|
+
- `ctr` is a fraction (`0.02` means 2%). It is computed from clicks and impressions when both exist; missing metrics remain absent.
|
|
220
|
+
- Google Search Analytics returns top rows and can omit lower-traffic rows. `performance` groups by day; `queries` and `pages` group by their names. [Google API reference](https://developers.google.com/webmaster-tools/v1/searchanalytics/query)
|
|
221
|
+
- Bing performance is daily. Its query and page statistics are weekly, and its `GetPageStats` uses a field named `Query` to hold the page URL. Bing traffic can include more than web search verticals, so cross-engine totals are directional comparisons. [Bing traffic reference](https://learn.microsoft.com/en-us/dotnet/api/microsoft.bing.webmaster.api.interfaces.iwebmasterapi.getrankandtrafficstats?view=bing-webmaster-dotnet), [page reference](https://learn.microsoft.com/en-us/dotnet/api/microsoft.bing.webmaster.api.interfaces.iwebmasterapi.getpagestats?view=bing-webmaster-dotnet)
|
|
222
|
+
- Yandex all-query history supports dated site totals. Popular queries support date ranges but return a bounded top set. URL-level query monitoring covers only the last two weeks, so longer `pages` requests return an `UNSUPPORTED` error for Yandex. [Yandex history](https://yandex.com/dev/webmaster/doc/en/reference/host-search-queries-history-all), [URL monitoring](https://yandex.ru/dev/webmaster/doc/ru/reference/host-query-analytics)
|
|
223
|
+
- Google URL inspection checks the version in Google's index. Yandex inspection can verify only URLs already present in Important Page Monitoring; other URLs return `unknown`. Bing URL information does not establish an indexing verdict, so the Bing adapter does not claim URL inspection support. [Google inspection](https://developers.google.com/webmaster-tools/v1/urlInspection.index/inspect), [Yandex important pages](https://yandex.com/dev/webmaster/doc/en/reference/host-id-important-urls)
|
|
224
|
+
|
|
225
|
+
No cross-engine position equivalence is assumed. Missing rows and `unknown` inspection states are never interpreted as proof of non-indexing.
|
|
226
|
+
|
|
227
|
+
## Development
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
npm run build
|
|
231
|
+
npm test
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
`src/core` contains the provider-independent model, credential interface, cache, service, scheduler, and pure analysis functions. Each adapter lives under `src/providers`. The `src/mcp` layer contains only tool definitions and mcponce integration. Provider HTTP calls accept an injected `fetch` for offline tests.
|
|
235
|
+
|
|
236
|
+
Set `MCP_BACKGROUND=0` to run one foreground MCP process for local debugging. Normal runs use mcponce's shared background mode.
|
|
237
|
+
|
|
238
|
+
SQLite defaults to the OS user cache/application-data directory. Override it with `WEBMASTER_MCP_DATA_DIR` for tests or isolated local use. History responses are cached for six hours, site lists for one hour, sitemaps for 30 minutes, and URL inspection for five minutes. Authentication failures are never cached.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createContext } from '../server/context.js';
|
|
3
|
+
import { GoogleProvider } from '../providers/google/index.js';
|
|
4
|
+
import { BingProvider } from '../providers/bing/index.js';
|
|
5
|
+
import { YandexProvider } from '../providers/yandex/index.js';
|
|
6
|
+
import { createServer } from '../mcp/index.js';
|
|
7
|
+
import { WebmasterError } from '../core/errors.js';
|
|
8
|
+
async function main() { const args = process.argv.slice(2); const command = args[0] ?? 'mcp'; if (command === 'mcp' || args.includes('--mcponce-background') || process.env.MCPONCE_BACKGROUND_SERVER === '1') {
|
|
9
|
+
await createServer().run();
|
|
10
|
+
return;
|
|
11
|
+
} const { service, store, accounts } = createContext(); const nameIndex = args.indexOf('--name'); const name = nameIndex >= 0 ? args[nameIndex + 1] ?? 'default' : 'default'; if (!/^[A-Za-z0-9_-]{1,40}$/.test(name))
|
|
12
|
+
throw new Error('Invalid account name.'); if (command === 'sites') {
|
|
13
|
+
process.stdout.write(`${JSON.stringify(await service.sites(), null, 2)}\n`);
|
|
14
|
+
return;
|
|
15
|
+
} if (command !== 'auth')
|
|
16
|
+
throw new Error('Usage: webmaster-mcp [mcp|sites|auth]'); const action = args[1]; if (action === 'status') {
|
|
17
|
+
for (const engine of ['google', 'bing', 'yandex'])
|
|
18
|
+
for (const account of accounts.names(engine)) {
|
|
19
|
+
const connected = !!await store.get(engine, account);
|
|
20
|
+
process.stdout.write(`${engine.padEnd(7)} ${account.padEnd(12)} ${connected ? '✓ connected' : '✗ not connected'}\n`);
|
|
21
|
+
}
|
|
22
|
+
return;
|
|
23
|
+
} if (action === 'logout') {
|
|
24
|
+
const engine = args[2];
|
|
25
|
+
if (!['google', 'bing', 'yandex'].includes(engine))
|
|
26
|
+
throw new Error('Choose google, bing, or yandex.');
|
|
27
|
+
await accounts.remove(engine, name);
|
|
28
|
+
process.stdout.write(`${engine}/${name} disconnected.\n`);
|
|
29
|
+
return;
|
|
30
|
+
} if (action === 'google') {
|
|
31
|
+
await new GoogleProvider(store, name, fetch, args.includes('--write')).authenticate();
|
|
32
|
+
accounts.add('google', name);
|
|
33
|
+
}
|
|
34
|
+
else if (action === 'bing') {
|
|
35
|
+
await new BingProvider(store, name).authenticate();
|
|
36
|
+
accounts.add('bing', name);
|
|
37
|
+
}
|
|
38
|
+
else if (action === 'yandex') {
|
|
39
|
+
await new YandexProvider(store, name).authenticate();
|
|
40
|
+
accounts.add('yandex', name);
|
|
41
|
+
}
|
|
42
|
+
else
|
|
43
|
+
throw new Error('Usage: webmaster-mcp auth [google|bing|yandex|status|logout]'); process.stdout.write(`${action}/${name} connected.\n`); }
|
|
44
|
+
main().catch(error => { if (error instanceof WebmasterError) {
|
|
45
|
+
process.stderr.write(`${error.code}: ${error.message}\n`);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
process.stderr.write(`ERROR: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
49
|
+
} process.exitCode = 1; });
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Engine, SearchMetric, UrlInspection } from '../types/index.js';
|
|
2
|
+
export interface Opportunity {
|
|
3
|
+
kind: 'low_ctr' | 'near_first_page' | 'declining_page' | 'declining_query' | 'impression_growth_without_clicks';
|
|
4
|
+
metric: SearchMetric;
|
|
5
|
+
previous?: SearchMetric;
|
|
6
|
+
reason: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function findCtrOpportunities(rows: SearchMetric[], config?: {
|
|
9
|
+
minImpressions: number;
|
|
10
|
+
maxCtr: number;
|
|
11
|
+
}): Opportunity[];
|
|
12
|
+
export declare function findNearFirstPage(rows: SearchMetric[], config?: {
|
|
13
|
+
minImpressions: number;
|
|
14
|
+
minPosition: number;
|
|
15
|
+
maxPosition: number;
|
|
16
|
+
}): Opportunity[];
|
|
17
|
+
export declare function findDecliningPages(current: SearchMetric[], previous: SearchMetric[], fraction?: number): Opportunity[];
|
|
18
|
+
export declare function findDecliningQueries(current: SearchMetric[], previous: SearchMetric[], fraction?: number): Opportunity[];
|
|
19
|
+
export declare function findImpressionGrowthWithoutClicks(current: SearchMetric[], previous: SearchMetric[], config?: {
|
|
20
|
+
minGrowth: number;
|
|
21
|
+
maxClickGrowth: number;
|
|
22
|
+
}): Opportunity[];
|
|
23
|
+
export declare function compareEngines(rows: SearchMetric[]): {
|
|
24
|
+
engine: Engine;
|
|
25
|
+
site: string;
|
|
26
|
+
clicks: number | undefined;
|
|
27
|
+
impressions: number | undefined;
|
|
28
|
+
ctr: number | undefined;
|
|
29
|
+
position: number | undefined;
|
|
30
|
+
semantics: string;
|
|
31
|
+
}[];
|
|
32
|
+
export declare function findEngineGaps(rows: SearchMetric[], engines: Engine[], minImpressions?: number): {
|
|
33
|
+
page: string;
|
|
34
|
+
visibleIn: Engine[];
|
|
35
|
+
absentFromResults: Engine[];
|
|
36
|
+
interpretation: string;
|
|
37
|
+
}[];
|
|
38
|
+
export declare function findIndexingGaps(inspections: UrlInspection[]): {
|
|
39
|
+
url: string;
|
|
40
|
+
indexed: Engine[];
|
|
41
|
+
notIndexed: Engine[];
|
|
42
|
+
evidence: UrlInspection[];
|
|
43
|
+
}[];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { aggregate } from '../normalize/index.js';
|
|
2
|
+
export function findCtrOpportunities(rows, config = { minImpressions: 100, maxCtr: 0.02 }) { return rows.filter(r => (r.impressions ?? 0) >= config.minImpressions && r.ctr !== undefined && r.ctr <= config.maxCtr).map(metric => ({ kind: 'low_ctr', metric, reason: `${metric.impressions} impressions with ${(metric.ctr * 100).toFixed(2)}% CTR` })); }
|
|
3
|
+
export function findNearFirstPage(rows, config = { minImpressions: 50, minPosition: 8, maxPosition: 20 }) { return rows.filter(r => (r.impressions ?? 0) >= config.minImpressions && r.position !== undefined && r.position >= config.minPosition && r.position <= config.maxPosition).map(metric => ({ kind: 'near_first_page', metric, reason: `Average position ${metric.position.toFixed(1)} with ${metric.impressions} impressions` })); }
|
|
4
|
+
function declines(current, previous, key, fraction) { const older = new Map(aggregate(previous, key).map(r => [`${r.engine}\0${r.site}\0${r[key]}`, r])); return aggregate(current, key).flatMap(metric => { const before = older.get(`${metric.engine}\0${metric.site}\0${metric[key]}`); if (!before || !before.clicks || metric.clicks === undefined || metric.clicks > before.clicks * (1 - fraction))
|
|
5
|
+
return []; return [{ kind: key === 'page' ? 'declining_page' : 'declining_query', metric, previous: before, reason: `Clicks fell from ${before.clicks} to ${metric.clicks}` }]; }); }
|
|
6
|
+
export function findDecliningPages(current, previous, fraction = 0.2) { return declines(current, previous, 'page', fraction); }
|
|
7
|
+
export function findDecliningQueries(current, previous, fraction = 0.2) { return declines(current, previous, 'query', fraction); }
|
|
8
|
+
export function findImpressionGrowthWithoutClicks(current, previous, config = { minGrowth: 0.3, maxClickGrowth: 0.05 }) { const older = new Map(aggregate(previous, 'query').map(r => [`${r.engine}\0${r.site}\0${r.query}`, r])); return aggregate(current, 'query').flatMap(metric => { const before = older.get(`${metric.engine}\0${metric.site}\0${metric.query}`); if (!before?.impressions || metric.impressions === undefined || metric.impressions < before.impressions * (1 + config.minGrowth) || (metric.clicks ?? 0) > (before.clicks ?? 0) * (1 + config.maxClickGrowth))
|
|
9
|
+
return []; return [{ kind: 'impression_growth_without_clicks', metric, previous: before, reason: `Impressions grew from ${before.impressions} to ${metric.impressions} while clicks stayed near ${before.clicks ?? 0}` }]; }); }
|
|
10
|
+
export function compareEngines(rows) { return aggregate(rows, 'engine').map(r => ({ engine: r.engine, site: r.site, clicks: r.clicks, impressions: r.impressions, ctr: r.ctr, position: r.position, semantics: 'Provider reported metrics; counting, position, and data freshness can differ.' })); }
|
|
11
|
+
export function findEngineGaps(rows, engines, minImpressions = 1) { const byPage = new Map(); for (const r of aggregate(rows.filter(x => !!x.page), 'page'))
|
|
12
|
+
byPage.set(r.page, [...(byPage.get(r.page) ?? []), r]); return [...byPage].flatMap(([page, found]) => { const visible = found.filter(r => (r.impressions ?? 0) >= minImpressions).map(r => r.engine); const absent = engines.filter(e => !found.some(r => r.engine === e)); return visible.length && absent.length ? [{ page, visibleIn: visible, absentFromResults: absent, interpretation: 'No row returned for this engine; this does not prove the URL is not indexed.' }] : []; }); }
|
|
13
|
+
export function findIndexingGaps(inspections) { const byUrl = new Map(); for (const i of inspections)
|
|
14
|
+
byUrl.set(i.url, [...(byUrl.get(i.url) ?? []), i]); return [...byUrl].flatMap(([url, items]) => { const indexed = items.filter(i => i.state === 'indexed').map(i => i.engine); const notIndexed = items.filter(i => i.state === 'not_indexed').map(i => i.engine); return indexed.length && notIndexed.length ? [{ url, indexed, notIndexed, evidence: items }] : []; }); }
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Engine } from '../types/index.js';
|
|
2
|
+
import type { CredentialStore } from './store.js';
|
|
3
|
+
import type { Cache } from '../cache/index.js';
|
|
4
|
+
export declare class AccountRegistry {
|
|
5
|
+
private cache;
|
|
6
|
+
private store;
|
|
7
|
+
constructor(cache: Cache, store: CredentialStore);
|
|
8
|
+
names(engine: Engine): string[];
|
|
9
|
+
add(engine: Engine, account: string): void;
|
|
10
|
+
remove(engine: Engine, account: string): Promise<void>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export class AccountRegistry {
|
|
2
|
+
cache;
|
|
3
|
+
store;
|
|
4
|
+
constructor(cache, store) {
|
|
5
|
+
this.cache = cache;
|
|
6
|
+
this.store = store;
|
|
7
|
+
}
|
|
8
|
+
names(engine) { const rows = this.cache.db.prepare('SELECT account FROM accounts WHERE provider=? ORDER BY account').all(engine); const names = rows.map(r => r.account); if (!names.includes('default'))
|
|
9
|
+
names.unshift('default'); return names; }
|
|
10
|
+
add(engine, account) { this.cache.db.prepare('INSERT OR IGNORE INTO accounts(provider,account) VALUES (?,?)').run(engine, account); }
|
|
11
|
+
async remove(engine, account) { await this.store.delete(engine, account); this.cache.db.prepare('DELETE FROM accounts WHERE provider=? AND account=?').run(engine, account); }
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function secretPrompt(label: string): Promise<string>;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
import { Writable } from 'node:stream';
|
|
3
|
+
export async function secretPrompt(label) { if (!process.stdin.isTTY)
|
|
4
|
+
throw new Error('Interactive terminal required.'); process.stderr.write(label); const output = new Writable({ write(_chunk, _encoding, callback) { callback(); } }); const rl = createInterface({ input: process.stdin, output, terminal: true }); return new Promise(resolve => rl.question('', answer => { rl.close(); process.stderr.write('\n'); resolve(answer.trim()); })); }
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface CredentialStore {
|
|
2
|
+
get(provider: string, account?: string): Promise<unknown | null>;
|
|
3
|
+
set(provider: string, account: string, credentials: unknown): Promise<void>;
|
|
4
|
+
delete(provider: string, account: string): Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
export declare class KeychainCredentialStore implements CredentialStore {
|
|
7
|
+
private entry;
|
|
8
|
+
get(provider: string, account?: string): Promise<unknown | null>;
|
|
9
|
+
set(provider: string, account: string, credentials: unknown): Promise<void>;
|
|
10
|
+
delete(provider: string, account: string): Promise<void>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Entry } from '@napi-rs/keyring';
|
|
2
|
+
export class KeychainCredentialStore {
|
|
3
|
+
entry(provider, account) { return new Entry('webmaster-mcp', `${provider}/${account}`); }
|
|
4
|
+
async get(provider, account = 'default') { const env = process.env[`WEBMASTER_${provider.toUpperCase()}_${account.toUpperCase().replace(/[^A-Z0-9]/g, '_')}`] ?? (account === 'default' ? process.env[`WEBMASTER_${provider.toUpperCase()}_TOKEN`] : undefined); if (env)
|
|
5
|
+
return provider === 'bing' ? { apiKey: env } : { accessToken: env }; try {
|
|
6
|
+
const value = this.entry(provider, account).getPassword();
|
|
7
|
+
return value ? JSON.parse(value) : null;
|
|
8
|
+
}
|
|
9
|
+
catch (e) {
|
|
10
|
+
if (String(e).toLowerCase().includes('no entry'))
|
|
11
|
+
return null;
|
|
12
|
+
throw new Error('OS credential store unavailable. Configure a desktop keyring or use a development environment variable.');
|
|
13
|
+
} }
|
|
14
|
+
async set(provider, account, credentials) { this.entry(provider, account).setPassword(JSON.stringify(credentials)); }
|
|
15
|
+
async delete(provider, account) { try {
|
|
16
|
+
this.entry(provider, account).deletePassword();
|
|
17
|
+
}
|
|
18
|
+
catch (e) {
|
|
19
|
+
if (!String(e).toLowerCase().includes('no entry'))
|
|
20
|
+
throw e;
|
|
21
|
+
} }
|
|
22
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import type { Engine } from '../types/index.js';
|
|
3
|
+
export declare function defaultDataPath(): string;
|
|
4
|
+
export declare function cacheKey(provider: Engine, account: string, site: string, operation: string, params: unknown): string;
|
|
5
|
+
export declare class Cache {
|
|
6
|
+
readonly db: Database.Database;
|
|
7
|
+
constructor(path?: string);
|
|
8
|
+
get<T>(key: string): T | null;
|
|
9
|
+
set(key: string, value: unknown, ttlMs: number): void;
|
|
10
|
+
close(): void;
|
|
11
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { mkdirSync } from 'node:fs';
|
|
4
|
+
import { dirname } from 'node:path';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
export function defaultDataPath() { if (process.env.WEBMASTER_MCP_DATA_DIR)
|
|
7
|
+
return `${process.env.WEBMASTER_MCP_DATA_DIR}/cache.db`; if (process.platform === 'darwin')
|
|
8
|
+
return `${homedir()}/Library/Application Support/webmaster-mcp/cache.db`; if (process.platform === 'win32')
|
|
9
|
+
return `${process.env.LOCALAPPDATA ?? homedir()}/webmaster-mcp/cache.db`; return `${process.env.XDG_CACHE_HOME ?? `${homedir()}/.cache`}/webmaster-mcp/cache.db`; }
|
|
10
|
+
function stable(value) { if (Array.isArray(value))
|
|
11
|
+
return value.map(stable); if (value && typeof value === 'object')
|
|
12
|
+
return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => [k, stable(v)])); return value; }
|
|
13
|
+
export function cacheKey(provider, account, site, operation, params) { return createHash('sha256').update(JSON.stringify(stable({ provider, account, site, operation, params }))).digest('hex'); }
|
|
14
|
+
export class Cache {
|
|
15
|
+
db;
|
|
16
|
+
constructor(path = defaultDataPath()) { if (path !== ':memory:')
|
|
17
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 }); this.db = new Database(path); this.db.pragma('busy_timeout = 5000'); this.db.pragma('journal_mode = WAL'); this.db.exec('CREATE TABLE IF NOT EXISTS accounts(provider TEXT NOT NULL,account TEXT NOT NULL,PRIMARY KEY(provider,account)); CREATE TABLE IF NOT EXISTS cache(key TEXT PRIMARY KEY,value TEXT NOT NULL,expires INTEGER NOT NULL)'); }
|
|
18
|
+
get(key) { const row = this.db.prepare('SELECT value FROM cache WHERE key=? AND expires>?').get(key, Date.now()); return row ? JSON.parse(row.value) : null; }
|
|
19
|
+
set(key, value, ttlMs) { this.db.prepare('INSERT INTO cache(key,value,expires) VALUES (?,?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value,expires=excluded.expires').run(key, JSON.stringify(value), Date.now() + ttlMs); }
|
|
20
|
+
close() { this.db.close(); }
|
|
21
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Engine, ProviderError } from './types/index.js';
|
|
2
|
+
export declare class WebmasterError extends Error {
|
|
3
|
+
code: ProviderError['code'];
|
|
4
|
+
status?: number | undefined;
|
|
5
|
+
constructor(code: ProviderError['code'], message: string, status?: number | undefined);
|
|
6
|
+
}
|
|
7
|
+
export declare function normalizeError(engine: Engine, account: string, error: unknown): ProviderError;
|
|
8
|
+
export declare function httpError(status: number): WebmasterError;
|
|
9
|
+
export declare function redact(value: string): string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export class WebmasterError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
status;
|
|
4
|
+
constructor(code, message, status) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.status = status;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export function normalizeError(engine, account, error) {
|
|
11
|
+
if (error instanceof WebmasterError)
|
|
12
|
+
return { engine, account, code: error.code, message: error.message };
|
|
13
|
+
return { engine, account, code: 'UPSTREAM_ERROR', message: 'Provider request failed. Check provider status and local configuration.' };
|
|
14
|
+
}
|
|
15
|
+
export function httpError(status) {
|
|
16
|
+
if (status === 401)
|
|
17
|
+
return new WebmasterError('AUTH_FAILED', 'Credentials were rejected or expired.', status);
|
|
18
|
+
if (status === 403)
|
|
19
|
+
return new WebmasterError('PERMISSION_DENIED', 'This account lacks access to the requested property.', status);
|
|
20
|
+
if (status === 429)
|
|
21
|
+
return new WebmasterError('RATE_LIMITED', 'Provider rate limit reached.', status);
|
|
22
|
+
return new WebmasterError('UPSTREAM_ERROR', `Provider returned HTTP ${status}.`, status);
|
|
23
|
+
}
|
|
24
|
+
export function redact(value) { return value.replace(/(Bearer|OAuth|apikey=|access_token=|refresh_token=)\s*[^\s&"']+/gi, '$1 [REDACTED]'); }
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { SearchMetric } from '../types/index.js';
|
|
2
|
+
export declare function finite(value: unknown): number | undefined;
|
|
3
|
+
export declare function metric(input: SearchMetric): SearchMetric;
|
|
4
|
+
export declare function aggregate(rows: SearchMetric[], key: 'engine' | 'page' | 'query'): SearchMetric[];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function finite(value) { const n = typeof value === 'number' ? value : typeof value === 'string' && value.trim() ? Number(value) : NaN; return Number.isFinite(n) ? n : undefined; }
|
|
2
|
+
export function metric(input) {
|
|
3
|
+
const clicks = finite(input.clicks), impressions = finite(input.impressions);
|
|
4
|
+
const ctr = finite(input.ctr) ?? (clicks !== undefined && impressions !== undefined && impressions > 0 ? clicks / impressions : undefined);
|
|
5
|
+
return { ...input, clicks, impressions, ctr, position: finite(input.position) };
|
|
6
|
+
}
|
|
7
|
+
export function aggregate(rows, key) {
|
|
8
|
+
const groups = new Map();
|
|
9
|
+
for (const row of rows) {
|
|
10
|
+
const value = String(row[key] ?? '');
|
|
11
|
+
const id = `${row.engine}\0${row.site}\0${value}`;
|
|
12
|
+
groups.set(id, [...(groups.get(id) ?? []), row]);
|
|
13
|
+
}
|
|
14
|
+
return [...groups.values()].map(group => { const first = group[0]; const clicks = group.some(r => r.clicks !== undefined) ? group.reduce((n, r) => n + (r.clicks ?? 0), 0) : undefined; const impressions = group.some(r => r.impressions !== undefined) ? group.reduce((n, r) => n + (r.impressions ?? 0), 0) : undefined; const positions = group.filter(r => r.position !== undefined); const weight = positions.reduce((n, r) => n + (r.impressions ?? 1), 0); return metric({ engine: first.engine, site: first.site, account: first.account, [key]: first[key], clicks, impressions, position: weight ? positions.reduce((n, r) => n + r.position * (r.impressions ?? 1), 0) / weight : undefined }); });
|
|
15
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Engine } from './types/index.js';
|
|
2
|
+
export interface SchedulerPolicy {
|
|
3
|
+
concurrency: number;
|
|
4
|
+
minimumDelay: number;
|
|
5
|
+
retries: number;
|
|
6
|
+
}
|
|
7
|
+
export declare class RequestScheduler {
|
|
8
|
+
private policy;
|
|
9
|
+
private active;
|
|
10
|
+
private next;
|
|
11
|
+
private queue;
|
|
12
|
+
constructor(policy: SchedulerPolicy);
|
|
13
|
+
private slot;
|
|
14
|
+
run<T>(fn: () => Promise<T>): Promise<T>;
|
|
15
|
+
}
|
|
16
|
+
export declare const schedules: Record<Engine, SchedulerPolicy>;
|
|
17
|
+
export declare function providerScheduler(engine: Engine): RequestScheduler;
|
|
18
|
+
export declare function jsonFetch(fetcher: typeof fetch, url: string, options?: RequestInit): Promise<any>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { httpError, WebmasterError } from './errors.js';
|
|
2
|
+
export class RequestScheduler {
|
|
3
|
+
policy;
|
|
4
|
+
active = 0;
|
|
5
|
+
next = 0;
|
|
6
|
+
queue = [];
|
|
7
|
+
constructor(policy) {
|
|
8
|
+
this.policy = policy;
|
|
9
|
+
}
|
|
10
|
+
async slot() { if (this.active >= this.policy.concurrency)
|
|
11
|
+
await new Promise(resolve => this.queue.push(resolve));
|
|
12
|
+
else
|
|
13
|
+
this.active++; const delay = Math.max(0, this.next - Date.now()); this.next = Math.max(Date.now(), this.next) + this.policy.minimumDelay; if (delay)
|
|
14
|
+
await new Promise(r => setTimeout(r, delay)); return () => { const next = this.queue.shift(); if (next)
|
|
15
|
+
next();
|
|
16
|
+
else
|
|
17
|
+
this.active--; }; }
|
|
18
|
+
async run(fn) { const release = await this.slot(); try {
|
|
19
|
+
for (let attempt = 0;; attempt++) {
|
|
20
|
+
try {
|
|
21
|
+
return await fn();
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
const retryable = e instanceof WebmasterError ? e.code === 'RATE_LIMITED' || e.code === 'NETWORK_ERROR' || (e.status !== undefined && e.status >= 500) : e instanceof TypeError;
|
|
25
|
+
if (!retryable || attempt >= this.policy.retries)
|
|
26
|
+
throw e;
|
|
27
|
+
await new Promise(r => setTimeout(r, Math.min(5000, 250 * 2 ** attempt + Math.random() * 100)));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
release();
|
|
33
|
+
} }
|
|
34
|
+
}
|
|
35
|
+
export const schedules = { google: { concurrency: 3, minimumDelay: 100, retries: 2 }, bing: { concurrency: 2, minimumDelay: 250, retries: 2 }, yandex: { concurrency: 2, minimumDelay: 200, retries: 2 } };
|
|
36
|
+
const shared = new Map();
|
|
37
|
+
export function providerScheduler(engine) { let scheduler = shared.get(engine); if (!scheduler) {
|
|
38
|
+
scheduler = new RequestScheduler(schedules[engine]);
|
|
39
|
+
shared.set(engine, scheduler);
|
|
40
|
+
} return scheduler; }
|
|
41
|
+
export async function jsonFetch(fetcher, url, options = {}) { let response; try {
|
|
42
|
+
response = await fetcher(url, { ...options, signal: options.signal ?? AbortSignal.timeout(15000) });
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
throw new WebmasterError('NETWORK_ERROR', 'Provider network request failed.');
|
|
46
|
+
} if (!response.ok)
|
|
47
|
+
throw httpError(response.status); try {
|
|
48
|
+
return await response.json();
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
throw new WebmasterError('UPSTREAM_ERROR', 'Provider returned invalid JSON.');
|
|
52
|
+
} }
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Capability, Engine, PartialResult, PerformanceQuery, SearchMetric, UrlInspection, WebmasterProvider } from './types/index.js';
|
|
2
|
+
import { Cache } from './cache/index.js';
|
|
3
|
+
export declare class WebmasterService {
|
|
4
|
+
readonly providers: WebmasterProvider[];
|
|
5
|
+
readonly cache: Cache;
|
|
6
|
+
private providerFactory?;
|
|
7
|
+
constructor(providers: WebmasterProvider[], cache: Cache, providerFactory?: (() => WebmasterProvider[]) | undefined);
|
|
8
|
+
private selected;
|
|
9
|
+
private oneAccount;
|
|
10
|
+
collect<T>(capability: Capability, fn: (p: WebmasterProvider) => Promise<T[]>, engines?: Engine[], account?: string): Promise<PartialResult<T>>;
|
|
11
|
+
sites(): Promise<{
|
|
12
|
+
results: {
|
|
13
|
+
site: string;
|
|
14
|
+
engines: Engine[];
|
|
15
|
+
accounts: Partial<Record<Engine, string[]>>;
|
|
16
|
+
}[];
|
|
17
|
+
errors: import("./types/index.js").ProviderError[];
|
|
18
|
+
}>;
|
|
19
|
+
private cached;
|
|
20
|
+
metrics(operation: 'performance' | 'queries' | 'pages', input: PerformanceQuery, engines?: Engine[]): Promise<PartialResult<SearchMetric>>;
|
|
21
|
+
sitemaps(site: string, engines?: Engine[], account?: string): Promise<PartialResult<{
|
|
22
|
+
engine: Engine;
|
|
23
|
+
url: string;
|
|
24
|
+
submitted?: string;
|
|
25
|
+
status?: string;
|
|
26
|
+
site: string;
|
|
27
|
+
account: string;
|
|
28
|
+
}>>;
|
|
29
|
+
inspect(site: string, url: string, engines?: Engine[], account?: string): Promise<PartialResult<UrlInspection>>;
|
|
30
|
+
}
|