@appsyogi/adsense-mcp-server 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 +210 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +1927 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +342 -0
- package/dist/index.js +1645 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
|
@@ -0,0 +1,1927 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/cli/init.ts
|
|
7
|
+
import * as fs2 from "fs";
|
|
8
|
+
import * as path2 from "path";
|
|
9
|
+
import * as readline from "readline";
|
|
10
|
+
import { OAuth2Client } from "google-auth-library";
|
|
11
|
+
|
|
12
|
+
// src/auth/tokenStore.ts
|
|
13
|
+
import * as fs from "fs/promises";
|
|
14
|
+
import * as path from "path";
|
|
15
|
+
import envPaths from "env-paths";
|
|
16
|
+
var paths = envPaths("adsense-mcp");
|
|
17
|
+
var KEYTAR_SERVICE = "adsense-mcp";
|
|
18
|
+
var KEYTAR_ACCOUNT = "tokens";
|
|
19
|
+
async function ensureConfigDir() {
|
|
20
|
+
await fs.mkdir(paths.config, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
async function loadConfig() {
|
|
23
|
+
await ensureConfigDir();
|
|
24
|
+
const configPath = path.join(paths.config, "config.json");
|
|
25
|
+
try {
|
|
26
|
+
const content = await fs.readFile(configPath, "utf-8");
|
|
27
|
+
return JSON.parse(content);
|
|
28
|
+
} catch {
|
|
29
|
+
return {
|
|
30
|
+
authType: "oauth",
|
|
31
|
+
scope: "readonly"
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function saveConfig(config) {
|
|
36
|
+
await ensureConfigDir();
|
|
37
|
+
const configPath = path.join(paths.config, "config.json");
|
|
38
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
|
|
39
|
+
}
|
|
40
|
+
async function loadTokens() {
|
|
41
|
+
try {
|
|
42
|
+
const keytar = await import("keytar");
|
|
43
|
+
const stored = await keytar.getPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNT);
|
|
44
|
+
if (stored) {
|
|
45
|
+
return JSON.parse(stored);
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
await ensureConfigDir();
|
|
50
|
+
const tokensPath = path.join(paths.config, "tokens.json");
|
|
51
|
+
try {
|
|
52
|
+
const content = await fs.readFile(tokensPath, "utf-8");
|
|
53
|
+
return JSON.parse(content);
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function saveTokens(tokens) {
|
|
59
|
+
const tokenString = JSON.stringify(tokens);
|
|
60
|
+
try {
|
|
61
|
+
const keytar = await import("keytar");
|
|
62
|
+
await keytar.setPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNT, tokenString);
|
|
63
|
+
return;
|
|
64
|
+
} catch {
|
|
65
|
+
}
|
|
66
|
+
await ensureConfigDir();
|
|
67
|
+
const tokensPath = path.join(paths.config, "tokens.json");
|
|
68
|
+
await fs.writeFile(tokensPath, tokenString, { mode: 384 });
|
|
69
|
+
}
|
|
70
|
+
function getCachePath() {
|
|
71
|
+
return path.join(paths.config, "cache.sqlite");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/types.ts
|
|
75
|
+
var ADSENSE_SCOPES = {
|
|
76
|
+
readonly: ["https://www.googleapis.com/auth/adsense.readonly"]
|
|
77
|
+
// Full scope available but NOT recommended for MCP use:
|
|
78
|
+
// full: ['https://www.googleapis.com/auth/adsense'],
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// src/cli/init.ts
|
|
82
|
+
async function runInit(options) {
|
|
83
|
+
console.log("\u{1F680} AdSense MCP Server Setup\n");
|
|
84
|
+
if (options.serviceAccount) {
|
|
85
|
+
await setupServiceAccount(options.serviceAccount);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const clientId = options.clientId || await prompt("Enter your OAuth Client ID: ");
|
|
89
|
+
const clientSecret = options.clientSecret || await prompt("Enter your OAuth Client Secret: ");
|
|
90
|
+
if (!clientId || !clientSecret) {
|
|
91
|
+
console.error("\u274C Client ID and Client Secret are required");
|
|
92
|
+
console.log("\nTo create OAuth credentials:");
|
|
93
|
+
console.log("1. Go to https://console.cloud.google.com/apis/credentials");
|
|
94
|
+
console.log("2. Create an OAuth 2.0 Client ID (Desktop app)");
|
|
95
|
+
console.log("3. Enable the AdSense Management API");
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
const configDir = path2.join(process.env.HOME || "", ".config", "adsense-mcp");
|
|
99
|
+
fs2.mkdirSync(configDir, { recursive: true });
|
|
100
|
+
const credentialsPath = path2.join(configDir, "credentials.json");
|
|
101
|
+
fs2.writeFileSync(credentialsPath, JSON.stringify({
|
|
102
|
+
installed: {
|
|
103
|
+
client_id: clientId,
|
|
104
|
+
client_secret: clientSecret,
|
|
105
|
+
redirect_uris: ["http://localhost:3000/oauth/callback", "urn:ietf:wg:oauth:2.0:oob"]
|
|
106
|
+
}
|
|
107
|
+
}, null, 2));
|
|
108
|
+
console.log(`\u2705 Credentials saved to ${credentialsPath}
|
|
109
|
+
`);
|
|
110
|
+
await performOAuthFlow(clientId, clientSecret);
|
|
111
|
+
}
|
|
112
|
+
async function setupServiceAccount(keyPath) {
|
|
113
|
+
console.log("Setting up service account authentication...\n");
|
|
114
|
+
if (!fs2.existsSync(keyPath)) {
|
|
115
|
+
console.error(`\u274C Service account key file not found: ${keyPath}`);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const keyContent = JSON.parse(fs2.readFileSync(keyPath, "utf-8"));
|
|
120
|
+
if (!keyContent.client_email || !keyContent.private_key) {
|
|
121
|
+
throw new Error("Invalid service account key file");
|
|
122
|
+
}
|
|
123
|
+
const configDir = path2.join(process.env.HOME || "", ".config", "adsense-mcp");
|
|
124
|
+
fs2.mkdirSync(configDir, { recursive: true });
|
|
125
|
+
const destPath = path2.join(configDir, "service-account.json");
|
|
126
|
+
fs2.copyFileSync(keyPath, destPath);
|
|
127
|
+
console.log(`\u2705 Service account key saved to ${destPath}`);
|
|
128
|
+
console.log(`\u{1F4E7} Service account: ${keyContent.client_email}
|
|
129
|
+
`);
|
|
130
|
+
console.log("\u26A0\uFE0F Important: Make sure this service account has been granted");
|
|
131
|
+
console.log(" access to your AdSense account via domain-wide delegation.\n");
|
|
132
|
+
console.log("Run `adsense-mcp doctor` to verify the setup.");
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error(`\u274C Failed to setup service account: ${error.message}`);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async function performOAuthFlow(clientId, clientSecret) {
|
|
139
|
+
const oauth2Client = new OAuth2Client(
|
|
140
|
+
clientId,
|
|
141
|
+
clientSecret,
|
|
142
|
+
"urn:ietf:wg:oauth:2.0:oob"
|
|
143
|
+
);
|
|
144
|
+
const authUrl = oauth2Client.generateAuthUrl({
|
|
145
|
+
access_type: "offline",
|
|
146
|
+
scope: ADSENSE_SCOPES.readonly,
|
|
147
|
+
prompt: "consent"
|
|
148
|
+
});
|
|
149
|
+
console.log("\u{1F4CB} Open this URL in your browser to authorize:\n");
|
|
150
|
+
console.log(authUrl);
|
|
151
|
+
console.log("\n");
|
|
152
|
+
const code = await prompt("Enter the authorization code: ");
|
|
153
|
+
try {
|
|
154
|
+
const { tokens } = await oauth2Client.getToken(code);
|
|
155
|
+
if (!tokens.access_token) {
|
|
156
|
+
throw new Error("No access token received");
|
|
157
|
+
}
|
|
158
|
+
await saveConfig({
|
|
159
|
+
authType: "oauth",
|
|
160
|
+
clientId,
|
|
161
|
+
clientSecret,
|
|
162
|
+
scope: "readonly"
|
|
163
|
+
});
|
|
164
|
+
await saveTokens({
|
|
165
|
+
accessToken: tokens.access_token,
|
|
166
|
+
refreshToken: tokens.refresh_token ?? void 0,
|
|
167
|
+
expiryDate: tokens.expiry_date ?? void 0
|
|
168
|
+
});
|
|
169
|
+
console.log("\n\u2705 Authentication successful!");
|
|
170
|
+
console.log("\u{1F510} Tokens saved securely\n");
|
|
171
|
+
console.log("Run `adsense-mcp doctor` to verify the setup.");
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.error(`
|
|
174
|
+
\u274C Authentication failed: ${error.message}`);
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function prompt(question) {
|
|
179
|
+
const rl = readline.createInterface({
|
|
180
|
+
input: process.stdin,
|
|
181
|
+
output: process.stdout
|
|
182
|
+
});
|
|
183
|
+
return new Promise((resolve) => {
|
|
184
|
+
rl.question(question, (answer) => {
|
|
185
|
+
rl.close();
|
|
186
|
+
resolve(answer.trim());
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// src/cli/doctor.ts
|
|
192
|
+
import * as fs4 from "fs";
|
|
193
|
+
import * as path3 from "path";
|
|
194
|
+
|
|
195
|
+
// src/auth/oauth.ts
|
|
196
|
+
import { OAuth2Client as OAuth2Client2 } from "google-auth-library";
|
|
197
|
+
async function createOAuthClient() {
|
|
198
|
+
const config = await loadConfig();
|
|
199
|
+
if (config.authType !== "oauth") {
|
|
200
|
+
throw new Error("OAuth not configured. Run `adsense-mcp init` to set up OAuth.");
|
|
201
|
+
}
|
|
202
|
+
const oauth2Client = new OAuth2Client2({
|
|
203
|
+
clientId: config.clientId,
|
|
204
|
+
clientSecret: config.clientSecret
|
|
205
|
+
});
|
|
206
|
+
const tokens = await loadTokens();
|
|
207
|
+
if (!tokens) {
|
|
208
|
+
throw new Error("No tokens found. Run `adsense-mcp init` to authenticate.");
|
|
209
|
+
}
|
|
210
|
+
oauth2Client.setCredentials({
|
|
211
|
+
access_token: tokens.accessToken,
|
|
212
|
+
refresh_token: tokens.refreshToken,
|
|
213
|
+
expiry_date: tokens.expiryDate
|
|
214
|
+
});
|
|
215
|
+
oauth2Client.on("tokens", async (newTokens) => {
|
|
216
|
+
await saveTokens({
|
|
217
|
+
accessToken: newTokens.access_token || tokens.accessToken,
|
|
218
|
+
refreshToken: newTokens.refresh_token || tokens.refreshToken,
|
|
219
|
+
expiryDate: newTokens.expiry_date || Date.now() + 36e5
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
return oauth2Client;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// src/auth/serviceAccount.ts
|
|
226
|
+
import { google } from "googleapis";
|
|
227
|
+
import * as fs3 from "fs/promises";
|
|
228
|
+
async function createServiceAccountClient() {
|
|
229
|
+
const config = await loadConfig();
|
|
230
|
+
if (config.authType !== "service-account") {
|
|
231
|
+
throw new Error("Service account not configured. Run `adsense-mcp init --service-account <path>` to set up.");
|
|
232
|
+
}
|
|
233
|
+
if (!config.serviceAccountPath) {
|
|
234
|
+
throw new Error("Service account path not configured.");
|
|
235
|
+
}
|
|
236
|
+
const keyFileContent = await fs3.readFile(config.serviceAccountPath, "utf-8");
|
|
237
|
+
const keyFile = JSON.parse(keyFileContent);
|
|
238
|
+
const auth = new google.auth.JWT({
|
|
239
|
+
email: keyFile.client_email,
|
|
240
|
+
key: keyFile.private_key,
|
|
241
|
+
scopes: ADSENSE_SCOPES[config.scope]
|
|
242
|
+
});
|
|
243
|
+
return auth;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// src/adsense/client.ts
|
|
247
|
+
import { google as google2 } from "googleapis";
|
|
248
|
+
|
|
249
|
+
// src/adsense/rateLimiter.ts
|
|
250
|
+
var MAX_REQUESTS_PER_MINUTE = 100;
|
|
251
|
+
var MAX_RETRIES = 5;
|
|
252
|
+
var BASE_DELAY_MS = 1e3;
|
|
253
|
+
var MAX_DELAY_MS = 32e3;
|
|
254
|
+
var RateLimiter = class {
|
|
255
|
+
requestTimestamps = [];
|
|
256
|
+
/**
|
|
257
|
+
* Wait if necessary to stay under rate limit
|
|
258
|
+
*/
|
|
259
|
+
async throttle() {
|
|
260
|
+
const now = Date.now();
|
|
261
|
+
const oneMinuteAgo = now - 6e4;
|
|
262
|
+
this.requestTimestamps = this.requestTimestamps.filter(
|
|
263
|
+
(ts) => ts > oneMinuteAgo
|
|
264
|
+
);
|
|
265
|
+
if (this.requestTimestamps.length >= MAX_REQUESTS_PER_MINUTE) {
|
|
266
|
+
const oldestInWindow = this.requestTimestamps[0];
|
|
267
|
+
const waitTime = oldestInWindow + 6e4 - now + 100;
|
|
268
|
+
if (waitTime > 0) {
|
|
269
|
+
await sleep(waitTime);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
this.requestTimestamps.push(Date.now());
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Get current request count in the last minute
|
|
276
|
+
*/
|
|
277
|
+
getRequestCount() {
|
|
278
|
+
const oneMinuteAgo = Date.now() - 6e4;
|
|
279
|
+
return this.requestTimestamps.filter((ts) => ts > oneMinuteAgo).length;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Check if we're close to the rate limit
|
|
283
|
+
*/
|
|
284
|
+
isNearLimit() {
|
|
285
|
+
return this.getRequestCount() >= MAX_REQUESTS_PER_MINUTE * 0.8;
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
async function withRetry(operation, retries = MAX_RETRIES) {
|
|
289
|
+
let lastError;
|
|
290
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
291
|
+
try {
|
|
292
|
+
return await operation();
|
|
293
|
+
} catch (error) {
|
|
294
|
+
lastError = error;
|
|
295
|
+
const isRateLimited = error.code === 429 || error.status === 429 || error.message?.includes("quota") || error.message?.includes("rate limit") || error.message?.includes("RATE_LIMIT_EXCEEDED");
|
|
296
|
+
const isRetryable = isRateLimited || error.code === 503 || error.code === 500 || error.status === 503 || error.status === 500 || error.code === "ECONNRESET" || error.code === "ETIMEDOUT";
|
|
297
|
+
if (!isRetryable || attempt === retries - 1) {
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
const delay = Math.min(
|
|
301
|
+
BASE_DELAY_MS * Math.pow(2, attempt) + Math.random() * 1e3,
|
|
302
|
+
MAX_DELAY_MS
|
|
303
|
+
);
|
|
304
|
+
console.error(
|
|
305
|
+
`Request failed (attempt ${attempt + 1}/${retries}), retrying in ${Math.round(delay)}ms...`
|
|
306
|
+
);
|
|
307
|
+
await sleep(delay);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
throw lastError || new Error("Max retries exceeded");
|
|
311
|
+
}
|
|
312
|
+
function sleep(ms) {
|
|
313
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
314
|
+
}
|
|
315
|
+
var rateLimiter = new RateLimiter();
|
|
316
|
+
|
|
317
|
+
// src/adsense/cache.ts
|
|
318
|
+
import Database from "better-sqlite3";
|
|
319
|
+
import * as crypto from "crypto";
|
|
320
|
+
var CacheTTL = {
|
|
321
|
+
TODAY_EARNINGS: 5 * 60 * 1e3,
|
|
322
|
+
// 5 minutes
|
|
323
|
+
YESTERDAY_EARNINGS: 60 * 60 * 1e3,
|
|
324
|
+
// 1 hour
|
|
325
|
+
HISTORICAL_REPORT: 24 * 60 * 60 * 1e3,
|
|
326
|
+
// 24 hours
|
|
327
|
+
ACCOUNTS: 24 * 60 * 60 * 1e3,
|
|
328
|
+
// 24 hours
|
|
329
|
+
SITES: 60 * 60 * 1e3,
|
|
330
|
+
// 1 hour
|
|
331
|
+
ALERTS: 15 * 60 * 1e3,
|
|
332
|
+
// 15 minutes
|
|
333
|
+
POLICY_ISSUES: 30 * 60 * 1e3,
|
|
334
|
+
// 30 minutes
|
|
335
|
+
PAYMENTS: 6 * 60 * 60 * 1e3,
|
|
336
|
+
// 6 hours
|
|
337
|
+
AD_UNITS: 60 * 60 * 1e3
|
|
338
|
+
// 1 hour
|
|
339
|
+
};
|
|
340
|
+
var CacheManager = class {
|
|
341
|
+
db;
|
|
342
|
+
constructor(dbPath) {
|
|
343
|
+
this.db = new Database(dbPath || getCachePath());
|
|
344
|
+
this.initSchema();
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Initialize database schema
|
|
348
|
+
*/
|
|
349
|
+
initSchema() {
|
|
350
|
+
this.db.exec(`
|
|
351
|
+
-- Report cache with TTL
|
|
352
|
+
CREATE TABLE IF NOT EXISTS report_cache (
|
|
353
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
354
|
+
cache_key TEXT UNIQUE NOT NULL,
|
|
355
|
+
account_id TEXT NOT NULL,
|
|
356
|
+
query_hash TEXT NOT NULL,
|
|
357
|
+
response_data TEXT NOT NULL,
|
|
358
|
+
created_at INTEGER NOT NULL,
|
|
359
|
+
expires_at INTEGER NOT NULL
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
CREATE INDEX IF NOT EXISTS idx_report_cache_key ON report_cache(cache_key);
|
|
363
|
+
CREATE INDEX IF NOT EXISTS idx_report_cache_expires ON report_cache(expires_at);
|
|
364
|
+
|
|
365
|
+
-- Account info cache
|
|
366
|
+
CREATE TABLE IF NOT EXISTS accounts_cache (
|
|
367
|
+
account_id TEXT PRIMARY KEY,
|
|
368
|
+
account_data TEXT NOT NULL,
|
|
369
|
+
updated_at INTEGER NOT NULL
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
-- Query history for analytics
|
|
373
|
+
CREATE TABLE IF NOT EXISTS query_history (
|
|
374
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
375
|
+
account_id TEXT NOT NULL,
|
|
376
|
+
tool_name TEXT NOT NULL,
|
|
377
|
+
query_params TEXT,
|
|
378
|
+
executed_at INTEGER NOT NULL,
|
|
379
|
+
response_time_ms INTEGER
|
|
380
|
+
);
|
|
381
|
+
`);
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Generate a cache key from query parameters
|
|
385
|
+
*/
|
|
386
|
+
generateCacheKey(prefix, params) {
|
|
387
|
+
const hash = crypto.createHash("md5").update(JSON.stringify(params)).digest("hex");
|
|
388
|
+
return `${prefix}:${hash}`;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Get cached data if not expired
|
|
392
|
+
*/
|
|
393
|
+
get(prefix, params) {
|
|
394
|
+
const cacheKey = this.generateCacheKey(prefix, params);
|
|
395
|
+
const now = Date.now();
|
|
396
|
+
const row = this.db.prepare("SELECT response_data, expires_at FROM report_cache WHERE cache_key = ?").get(cacheKey);
|
|
397
|
+
if (row && row.expires_at > now) {
|
|
398
|
+
return JSON.parse(row.response_data);
|
|
399
|
+
}
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Store data in cache
|
|
404
|
+
*/
|
|
405
|
+
set(prefix, params, data, ttl, accountId) {
|
|
406
|
+
const cacheKey = this.generateCacheKey(prefix, params);
|
|
407
|
+
const queryHash = crypto.createHash("md5").update(JSON.stringify(params)).digest("hex");
|
|
408
|
+
const now = Date.now();
|
|
409
|
+
this.db.prepare(`
|
|
410
|
+
INSERT OR REPLACE INTO report_cache
|
|
411
|
+
(cache_key, account_id, query_hash, response_data, created_at, expires_at)
|
|
412
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
413
|
+
`).run(cacheKey, accountId, queryHash, JSON.stringify(data), now, now + ttl);
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Clear expired cache entries
|
|
417
|
+
*/
|
|
418
|
+
clearExpired() {
|
|
419
|
+
const result = this.db.prepare("DELETE FROM report_cache WHERE expires_at < ?").run(Date.now());
|
|
420
|
+
return result.changes;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Clear all cache for an account
|
|
424
|
+
*/
|
|
425
|
+
clearAccount(accountId) {
|
|
426
|
+
const result = this.db.prepare("DELETE FROM report_cache WHERE account_id = ?").run(accountId);
|
|
427
|
+
return result.changes;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Clear all cache
|
|
431
|
+
*/
|
|
432
|
+
clearAll() {
|
|
433
|
+
this.db.exec("DELETE FROM report_cache");
|
|
434
|
+
this.db.exec("DELETE FROM accounts_cache");
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Record a query for analytics
|
|
438
|
+
*/
|
|
439
|
+
recordQuery(accountId, toolName, params, responseTimeMs) {
|
|
440
|
+
this.db.prepare(`
|
|
441
|
+
INSERT INTO query_history (account_id, tool_name, query_params, executed_at, response_time_ms)
|
|
442
|
+
VALUES (?, ?, ?, ?, ?)
|
|
443
|
+
`).run(accountId, toolName, JSON.stringify(params), Date.now(), responseTimeMs);
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Get cache statistics
|
|
447
|
+
*/
|
|
448
|
+
getStats() {
|
|
449
|
+
const now = Date.now();
|
|
450
|
+
const totalEntries = this.db.prepare("SELECT COUNT(*) as count FROM report_cache").get().count;
|
|
451
|
+
const expiredCount = this.db.prepare("SELECT COUNT(*) as count FROM report_cache WHERE expires_at < ?").get(now).count;
|
|
452
|
+
const sizeResult = this.db.prepare("SELECT SUM(LENGTH(response_data)) as size FROM report_cache").get();
|
|
453
|
+
return {
|
|
454
|
+
totalEntries,
|
|
455
|
+
totalSize: sizeResult.size || 0,
|
|
456
|
+
expiredCount
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Close database connection
|
|
461
|
+
*/
|
|
462
|
+
close() {
|
|
463
|
+
this.db.close();
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
var cacheInstance = null;
|
|
467
|
+
function getCache() {
|
|
468
|
+
if (!cacheInstance) {
|
|
469
|
+
cacheInstance = new CacheManager();
|
|
470
|
+
}
|
|
471
|
+
return cacheInstance;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// src/adsense/client.ts
|
|
475
|
+
var AdSenseClient = class _AdSenseClient {
|
|
476
|
+
adsense;
|
|
477
|
+
defaultAccountId;
|
|
478
|
+
constructor(adsense, defaultAccountId) {
|
|
479
|
+
this.adsense = adsense;
|
|
480
|
+
this.defaultAccountId = defaultAccountId;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Create an authenticated AdSense client
|
|
484
|
+
*/
|
|
485
|
+
static async create(accountIdOverride) {
|
|
486
|
+
const config = await loadConfig();
|
|
487
|
+
let auth;
|
|
488
|
+
if (config.authType === "service-account") {
|
|
489
|
+
auth = await createServiceAccountClient();
|
|
490
|
+
} else {
|
|
491
|
+
auth = await createOAuthClient();
|
|
492
|
+
}
|
|
493
|
+
const adsense = google2.adsense({
|
|
494
|
+
version: "v2",
|
|
495
|
+
auth
|
|
496
|
+
});
|
|
497
|
+
const accountId = accountIdOverride || config.defaultAccountId;
|
|
498
|
+
return new _AdSenseClient(adsense, accountId);
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Get the account ID to use (explicit > default > first available)
|
|
502
|
+
*/
|
|
503
|
+
async resolveAccountId(explicitId) {
|
|
504
|
+
if (explicitId) {
|
|
505
|
+
return explicitId.startsWith("accounts/") ? explicitId : `accounts/${explicitId}`;
|
|
506
|
+
}
|
|
507
|
+
if (this.defaultAccountId) {
|
|
508
|
+
return this.defaultAccountId.startsWith("accounts/") ? this.defaultAccountId : `accounts/${this.defaultAccountId}`;
|
|
509
|
+
}
|
|
510
|
+
const accounts = await this.listAccounts();
|
|
511
|
+
if (accounts.length === 0) {
|
|
512
|
+
throw new Error("No AdSense accounts found");
|
|
513
|
+
}
|
|
514
|
+
return accounts[0].name;
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Extract publisher ID from account name
|
|
518
|
+
*/
|
|
519
|
+
extractPubId(accountName) {
|
|
520
|
+
return accountName.replace("accounts/", "");
|
|
521
|
+
}
|
|
522
|
+
// ==================== Account Operations ====================
|
|
523
|
+
/**
|
|
524
|
+
* List all AdSense accounts
|
|
525
|
+
*/
|
|
526
|
+
async listAccounts() {
|
|
527
|
+
const cache = getCache();
|
|
528
|
+
const cacheKey = "accounts";
|
|
529
|
+
const cached = cache.get(cacheKey, {});
|
|
530
|
+
if (cached) {
|
|
531
|
+
return cached;
|
|
532
|
+
}
|
|
533
|
+
await rateLimiter.throttle();
|
|
534
|
+
const response = await withRetry(
|
|
535
|
+
() => this.adsense.accounts.list()
|
|
536
|
+
);
|
|
537
|
+
const accounts = response.data.accounts || [];
|
|
538
|
+
cache.set(cacheKey, {}, accounts, CacheTTL.ACCOUNTS, "global");
|
|
539
|
+
return accounts;
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Get a specific account
|
|
543
|
+
*/
|
|
544
|
+
async getAccount(accountId) {
|
|
545
|
+
const resolvedId = await this.resolveAccountId(accountId);
|
|
546
|
+
await rateLimiter.throttle();
|
|
547
|
+
try {
|
|
548
|
+
const response = await withRetry(
|
|
549
|
+
() => this.adsense.accounts.get({ name: resolvedId })
|
|
550
|
+
);
|
|
551
|
+
return response.data;
|
|
552
|
+
} catch {
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
// ==================== Sites Operations ====================
|
|
557
|
+
/**
|
|
558
|
+
* List all sites for an account
|
|
559
|
+
*/
|
|
560
|
+
async listSites(accountId) {
|
|
561
|
+
const resolvedId = await this.resolveAccountId(accountId);
|
|
562
|
+
const cache = getCache();
|
|
563
|
+
const params = { accountId: resolvedId };
|
|
564
|
+
const cached = cache.get("sites", params);
|
|
565
|
+
if (cached) {
|
|
566
|
+
return cached;
|
|
567
|
+
}
|
|
568
|
+
await rateLimiter.throttle();
|
|
569
|
+
const response = await withRetry(
|
|
570
|
+
() => this.adsense.accounts.sites.list({ parent: resolvedId })
|
|
571
|
+
);
|
|
572
|
+
const sites = response.data.sites || [];
|
|
573
|
+
cache.set("sites", params, sites, CacheTTL.SITES, this.extractPubId(resolvedId));
|
|
574
|
+
return sites;
|
|
575
|
+
}
|
|
576
|
+
// ==================== Alerts Operations ====================
|
|
577
|
+
/**
|
|
578
|
+
* List alerts for an account
|
|
579
|
+
*/
|
|
580
|
+
async listAlerts(accountId) {
|
|
581
|
+
const resolvedId = await this.resolveAccountId(accountId);
|
|
582
|
+
const cache = getCache();
|
|
583
|
+
const params = { accountId: resolvedId };
|
|
584
|
+
const cached = cache.get("alerts", params);
|
|
585
|
+
if (cached) {
|
|
586
|
+
return cached;
|
|
587
|
+
}
|
|
588
|
+
await rateLimiter.throttle();
|
|
589
|
+
const response = await withRetry(
|
|
590
|
+
() => this.adsense.accounts.alerts.list({ parent: resolvedId })
|
|
591
|
+
);
|
|
592
|
+
const alerts = response.data.alerts || [];
|
|
593
|
+
cache.set("alerts", params, alerts, CacheTTL.ALERTS, this.extractPubId(resolvedId));
|
|
594
|
+
return alerts;
|
|
595
|
+
}
|
|
596
|
+
// ==================== Policy Issues Operations ====================
|
|
597
|
+
/**
|
|
598
|
+
* List policy issues for an account
|
|
599
|
+
*/
|
|
600
|
+
async listPolicyIssues(accountId) {
|
|
601
|
+
const resolvedId = await this.resolveAccountId(accountId);
|
|
602
|
+
const cache = getCache();
|
|
603
|
+
const params = { accountId: resolvedId };
|
|
604
|
+
const cached = cache.get("policyIssues", params);
|
|
605
|
+
if (cached) {
|
|
606
|
+
return cached;
|
|
607
|
+
}
|
|
608
|
+
await rateLimiter.throttle();
|
|
609
|
+
const response = await withRetry(
|
|
610
|
+
() => this.adsense.accounts.policyIssues.list({ parent: resolvedId })
|
|
611
|
+
);
|
|
612
|
+
const issues = response.data.policyIssues || [];
|
|
613
|
+
cache.set("policyIssues", params, issues, CacheTTL.POLICY_ISSUES, this.extractPubId(resolvedId));
|
|
614
|
+
return issues;
|
|
615
|
+
}
|
|
616
|
+
// ==================== Payments Operations ====================
|
|
617
|
+
/**
|
|
618
|
+
* List payments for an account
|
|
619
|
+
*/
|
|
620
|
+
async listPayments(accountId) {
|
|
621
|
+
const resolvedId = await this.resolveAccountId(accountId);
|
|
622
|
+
const cache = getCache();
|
|
623
|
+
const params = { accountId: resolvedId };
|
|
624
|
+
const cached = cache.get("payments", params);
|
|
625
|
+
if (cached) {
|
|
626
|
+
return cached;
|
|
627
|
+
}
|
|
628
|
+
await rateLimiter.throttle();
|
|
629
|
+
const response = await withRetry(
|
|
630
|
+
() => this.adsense.accounts.payments.list({ parent: resolvedId })
|
|
631
|
+
);
|
|
632
|
+
const payments = response.data.payments || [];
|
|
633
|
+
cache.set("payments", params, payments, CacheTTL.PAYMENTS, this.extractPubId(resolvedId));
|
|
634
|
+
return payments;
|
|
635
|
+
}
|
|
636
|
+
// ==================== Ad Client Operations ====================
|
|
637
|
+
/**
|
|
638
|
+
* List ad clients for an account
|
|
639
|
+
*/
|
|
640
|
+
async listAdClients(accountId) {
|
|
641
|
+
const resolvedId = await this.resolveAccountId(accountId);
|
|
642
|
+
await rateLimiter.throttle();
|
|
643
|
+
const response = await withRetry(
|
|
644
|
+
() => this.adsense.accounts.adclients.list({ parent: resolvedId })
|
|
645
|
+
);
|
|
646
|
+
return response.data.adClients || [];
|
|
647
|
+
}
|
|
648
|
+
// ==================== Ad Unit Operations ====================
|
|
649
|
+
/**
|
|
650
|
+
* List ad units for an account (across all ad clients)
|
|
651
|
+
*/
|
|
652
|
+
async listAdUnits(accountId) {
|
|
653
|
+
const resolvedId = await this.resolveAccountId(accountId);
|
|
654
|
+
const cache = getCache();
|
|
655
|
+
const params = { accountId: resolvedId };
|
|
656
|
+
const cached = cache.get("adUnits", params);
|
|
657
|
+
if (cached) {
|
|
658
|
+
return cached;
|
|
659
|
+
}
|
|
660
|
+
const adClients = await this.listAdClients(accountId);
|
|
661
|
+
const allAdUnits = [];
|
|
662
|
+
for (const client of adClients) {
|
|
663
|
+
await rateLimiter.throttle();
|
|
664
|
+
const response = await withRetry(
|
|
665
|
+
() => this.adsense.accounts.adclients.adunits.list({ parent: client.name })
|
|
666
|
+
);
|
|
667
|
+
allAdUnits.push(...response.data.adUnits || []);
|
|
668
|
+
}
|
|
669
|
+
cache.set("adUnits", params, allAdUnits, CacheTTL.AD_UNITS, this.extractPubId(resolvedId));
|
|
670
|
+
return allAdUnits;
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Get ad code for an ad unit
|
|
674
|
+
*/
|
|
675
|
+
async getAdCode(adClientId, adUnitId, accountId) {
|
|
676
|
+
const resolvedId = await this.resolveAccountId(accountId);
|
|
677
|
+
const adUnitName = `${resolvedId}/adclients/${adClientId}/adunits/${adUnitId}`;
|
|
678
|
+
await rateLimiter.throttle();
|
|
679
|
+
const response = await withRetry(
|
|
680
|
+
() => this.adsense.accounts.adclients.adunits.getAdcode({ name: adUnitName })
|
|
681
|
+
);
|
|
682
|
+
return response.data.adCode || "";
|
|
683
|
+
}
|
|
684
|
+
// ==================== Report Operations ====================
|
|
685
|
+
/**
|
|
686
|
+
* Generate a report
|
|
687
|
+
*/
|
|
688
|
+
async generateReport(query) {
|
|
689
|
+
const resolvedId = await this.resolveAccountId(query.accountId);
|
|
690
|
+
const cache = getCache();
|
|
691
|
+
const ttl = this.getReportTTL(query.startDate, query.endDate);
|
|
692
|
+
const cached = cache.get("report", { ...query, accountId: resolvedId });
|
|
693
|
+
if (cached) {
|
|
694
|
+
return cached;
|
|
695
|
+
}
|
|
696
|
+
await rateLimiter.throttle();
|
|
697
|
+
const params = {
|
|
698
|
+
account: resolvedId,
|
|
699
|
+
"dateRange": query.dateRange || "CUSTOM",
|
|
700
|
+
"startDate.year": parseInt(query.startDate.split("-")[0]),
|
|
701
|
+
"startDate.month": parseInt(query.startDate.split("-")[1]),
|
|
702
|
+
"startDate.day": parseInt(query.startDate.split("-")[2]),
|
|
703
|
+
"endDate.year": parseInt(query.endDate.split("-")[0]),
|
|
704
|
+
"endDate.month": parseInt(query.endDate.split("-")[1]),
|
|
705
|
+
"endDate.day": parseInt(query.endDate.split("-")[2])
|
|
706
|
+
};
|
|
707
|
+
if (query.dimensions && query.dimensions.length > 0) {
|
|
708
|
+
params.dimensions = query.dimensions;
|
|
709
|
+
}
|
|
710
|
+
params.metrics = query.metrics || [
|
|
711
|
+
"ESTIMATED_EARNINGS",
|
|
712
|
+
"IMPRESSIONS",
|
|
713
|
+
"CLICKS",
|
|
714
|
+
"PAGE_VIEWS",
|
|
715
|
+
"PAGE_VIEWS_CTR",
|
|
716
|
+
"PAGE_VIEWS_RPM"
|
|
717
|
+
];
|
|
718
|
+
if (query.orderBy) {
|
|
719
|
+
params.orderBy = query.orderBy.startsWith("-") ? `${query.orderBy.slice(1)} DESC` : `${query.orderBy} ASC`;
|
|
720
|
+
}
|
|
721
|
+
if (query.limit) {
|
|
722
|
+
params.limit = Math.min(query.limit, 1e5);
|
|
723
|
+
}
|
|
724
|
+
const response = await withRetry(
|
|
725
|
+
() => this.adsense.accounts.reports.generate(params)
|
|
726
|
+
);
|
|
727
|
+
const report = response.data;
|
|
728
|
+
cache.set("report", { ...query, accountId: resolvedId }, report, ttl, this.extractPubId(resolvedId));
|
|
729
|
+
return report;
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Generate a CSV report
|
|
733
|
+
*/
|
|
734
|
+
async generateCsvReport(query) {
|
|
735
|
+
const resolvedId = await this.resolveAccountId(query.accountId);
|
|
736
|
+
await rateLimiter.throttle();
|
|
737
|
+
const params = {
|
|
738
|
+
account: resolvedId,
|
|
739
|
+
"dateRange": query.dateRange || "CUSTOM",
|
|
740
|
+
"startDate.year": parseInt(query.startDate.split("-")[0]),
|
|
741
|
+
"startDate.month": parseInt(query.startDate.split("-")[1]),
|
|
742
|
+
"startDate.day": parseInt(query.startDate.split("-")[2]),
|
|
743
|
+
"endDate.year": parseInt(query.endDate.split("-")[0]),
|
|
744
|
+
"endDate.month": parseInt(query.endDate.split("-")[1]),
|
|
745
|
+
"endDate.day": parseInt(query.endDate.split("-")[2])
|
|
746
|
+
};
|
|
747
|
+
if (query.dimensions) {
|
|
748
|
+
params.dimensions = query.dimensions;
|
|
749
|
+
}
|
|
750
|
+
params.metrics = query.metrics || [
|
|
751
|
+
"ESTIMATED_EARNINGS",
|
|
752
|
+
"IMPRESSIONS",
|
|
753
|
+
"CLICKS",
|
|
754
|
+
"PAGE_VIEWS"
|
|
755
|
+
];
|
|
756
|
+
const response = await withRetry(
|
|
757
|
+
() => this.adsense.accounts.reports.generateCsv(params)
|
|
758
|
+
);
|
|
759
|
+
return response.data;
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Get earnings summary (today, yesterday, last 7 days, this month, last month)
|
|
763
|
+
*/
|
|
764
|
+
async getEarningsSummary(accountId) {
|
|
765
|
+
const resolvedId = await this.resolveAccountId(accountId);
|
|
766
|
+
const today = /* @__PURE__ */ new Date();
|
|
767
|
+
const yesterday = new Date(today);
|
|
768
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
769
|
+
const formatDate = (d) => d.toISOString().split("T")[0];
|
|
770
|
+
const todayStr = formatDate(today);
|
|
771
|
+
const yesterdayStr = formatDate(yesterday);
|
|
772
|
+
const last7DaysStart = new Date(today);
|
|
773
|
+
last7DaysStart.setDate(last7DaysStart.getDate() - 6);
|
|
774
|
+
const thisMonthStart = new Date(today.getFullYear(), today.getMonth(), 1);
|
|
775
|
+
const lastMonthStart = new Date(today.getFullYear(), today.getMonth() - 1, 1);
|
|
776
|
+
const lastMonthEnd = new Date(today.getFullYear(), today.getMonth(), 0);
|
|
777
|
+
const [todayReport, yesterdayReport, last7DaysReport, thisMonthReport, lastMonthReport] = await Promise.all([
|
|
778
|
+
this.generateReport({
|
|
779
|
+
accountId: resolvedId,
|
|
780
|
+
startDate: todayStr,
|
|
781
|
+
endDate: todayStr
|
|
782
|
+
}),
|
|
783
|
+
this.generateReport({
|
|
784
|
+
accountId: resolvedId,
|
|
785
|
+
startDate: yesterdayStr,
|
|
786
|
+
endDate: yesterdayStr
|
|
787
|
+
}),
|
|
788
|
+
this.generateReport({
|
|
789
|
+
accountId: resolvedId,
|
|
790
|
+
startDate: formatDate(last7DaysStart),
|
|
791
|
+
endDate: todayStr
|
|
792
|
+
}),
|
|
793
|
+
this.generateReport({
|
|
794
|
+
accountId: resolvedId,
|
|
795
|
+
startDate: formatDate(thisMonthStart),
|
|
796
|
+
endDate: todayStr
|
|
797
|
+
}),
|
|
798
|
+
this.generateReport({
|
|
799
|
+
accountId: resolvedId,
|
|
800
|
+
startDate: formatDate(lastMonthStart),
|
|
801
|
+
endDate: formatDate(lastMonthEnd)
|
|
802
|
+
})
|
|
803
|
+
]);
|
|
804
|
+
return {
|
|
805
|
+
today: this.extractEarningsPeriod(todayReport),
|
|
806
|
+
yesterday: this.extractEarningsPeriod(yesterdayReport),
|
|
807
|
+
last7Days: this.extractEarningsPeriod(last7DaysReport),
|
|
808
|
+
thisMonth: this.extractEarningsPeriod(thisMonthReport),
|
|
809
|
+
lastMonth: this.extractEarningsPeriod(lastMonthReport)
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Extract earnings data from a report response
|
|
814
|
+
*/
|
|
815
|
+
extractEarningsPeriod(report) {
|
|
816
|
+
const totals = report.totals?.cells || report.rows?.[0]?.cells || [];
|
|
817
|
+
const headers = report.headers || [];
|
|
818
|
+
const getValue = (metricName) => {
|
|
819
|
+
const index = headers.findIndex((h) => h.name === metricName);
|
|
820
|
+
if (index >= 0 && totals[index]) {
|
|
821
|
+
return parseFloat(totals[index].value) || 0;
|
|
822
|
+
}
|
|
823
|
+
return 0;
|
|
824
|
+
};
|
|
825
|
+
return {
|
|
826
|
+
earnings: getValue("ESTIMATED_EARNINGS"),
|
|
827
|
+
impressions: getValue("IMPRESSIONS"),
|
|
828
|
+
clicks: getValue("CLICKS"),
|
|
829
|
+
ctr: getValue("PAGE_VIEWS_CTR") || getValue("IMPRESSIONS_CTR"),
|
|
830
|
+
rpm: getValue("PAGE_VIEWS_RPM") || getValue("IMPRESSIONS_RPM"),
|
|
831
|
+
pageViews: getValue("PAGE_VIEWS")
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Determine appropriate cache TTL based on date range
|
|
836
|
+
*/
|
|
837
|
+
getReportTTL(startDate, endDate) {
|
|
838
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
839
|
+
const yesterday = /* @__PURE__ */ new Date();
|
|
840
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
841
|
+
const yesterdayStr = yesterday.toISOString().split("T")[0];
|
|
842
|
+
if (endDate === today || startDate === today) {
|
|
843
|
+
return CacheTTL.TODAY_EARNINGS;
|
|
844
|
+
}
|
|
845
|
+
if (endDate === yesterdayStr) {
|
|
846
|
+
return CacheTTL.YESTERDAY_EARNINGS;
|
|
847
|
+
}
|
|
848
|
+
return CacheTTL.HISTORICAL_REPORT;
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
// src/cli/doctor.ts
|
|
853
|
+
async function runDoctor() {
|
|
854
|
+
console.log("\u{1FA7A} AdSense MCP Server Health Check\n");
|
|
855
|
+
const results = [];
|
|
856
|
+
const configDir = path3.join(process.env.HOME || "", ".config", "adsense-mcp");
|
|
857
|
+
if (fs4.existsSync(configDir)) {
|
|
858
|
+
results.push({
|
|
859
|
+
name: "Config Directory",
|
|
860
|
+
status: "pass",
|
|
861
|
+
message: configDir
|
|
862
|
+
});
|
|
863
|
+
} else {
|
|
864
|
+
results.push({
|
|
865
|
+
name: "Config Directory",
|
|
866
|
+
status: "fail",
|
|
867
|
+
message: "Not found. Run `adsense-mcp init` first."
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
const credentialsPath = path3.join(configDir, "credentials.json");
|
|
871
|
+
const serviceAccountPath = path3.join(configDir, "service-account.json");
|
|
872
|
+
if (fs4.existsSync(serviceAccountPath)) {
|
|
873
|
+
results.push({
|
|
874
|
+
name: "Authentication",
|
|
875
|
+
status: "pass",
|
|
876
|
+
message: "Service account configured"
|
|
877
|
+
});
|
|
878
|
+
} else if (fs4.existsSync(credentialsPath)) {
|
|
879
|
+
results.push({
|
|
880
|
+
name: "Authentication",
|
|
881
|
+
status: "pass",
|
|
882
|
+
message: "OAuth credentials configured"
|
|
883
|
+
});
|
|
884
|
+
} else {
|
|
885
|
+
results.push({
|
|
886
|
+
name: "Authentication",
|
|
887
|
+
status: "fail",
|
|
888
|
+
message: "No credentials found. Run `adsense-mcp init` first."
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
try {
|
|
892
|
+
const config = await loadConfig();
|
|
893
|
+
let authClient;
|
|
894
|
+
if (config.authType === "service-account") {
|
|
895
|
+
authClient = await createServiceAccountClient();
|
|
896
|
+
} else {
|
|
897
|
+
authClient = await createOAuthClient();
|
|
898
|
+
}
|
|
899
|
+
if (authClient) {
|
|
900
|
+
results.push({
|
|
901
|
+
name: "Auth Client",
|
|
902
|
+
status: "pass",
|
|
903
|
+
message: "Successfully created auth client"
|
|
904
|
+
});
|
|
905
|
+
} else {
|
|
906
|
+
throw new Error("Auth client is null");
|
|
907
|
+
}
|
|
908
|
+
} catch (error) {
|
|
909
|
+
results.push({
|
|
910
|
+
name: "Auth Client",
|
|
911
|
+
status: "fail",
|
|
912
|
+
message: `Failed: ${error.message}`
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
try {
|
|
916
|
+
const client = await AdSenseClient.create();
|
|
917
|
+
const accounts = await client.listAccounts();
|
|
918
|
+
if (accounts.length > 0) {
|
|
919
|
+
results.push({
|
|
920
|
+
name: "AdSense API",
|
|
921
|
+
status: "pass",
|
|
922
|
+
message: `Found ${accounts.length} account(s)`
|
|
923
|
+
});
|
|
924
|
+
console.log("\n\u{1F4CA} AdSense Accounts:");
|
|
925
|
+
for (const account of accounts) {
|
|
926
|
+
const id = account.name.replace("accounts/", "");
|
|
927
|
+
console.log(` \u2022 ${id} - ${account.displayName || "Unnamed"}`);
|
|
928
|
+
}
|
|
929
|
+
} else {
|
|
930
|
+
results.push({
|
|
931
|
+
name: "AdSense API",
|
|
932
|
+
status: "warn",
|
|
933
|
+
message: "No accounts found"
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
} catch (error) {
|
|
937
|
+
results.push({
|
|
938
|
+
name: "AdSense API",
|
|
939
|
+
status: "fail",
|
|
940
|
+
message: `Failed: ${error.message}`
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
const cacheDir = path3.join(configDir, "cache");
|
|
944
|
+
if (fs4.existsSync(cacheDir)) {
|
|
945
|
+
const cacheFiles = fs4.readdirSync(cacheDir);
|
|
946
|
+
results.push({
|
|
947
|
+
name: "Cache",
|
|
948
|
+
status: "pass",
|
|
949
|
+
message: `${cacheFiles.length} cache file(s)`
|
|
950
|
+
});
|
|
951
|
+
} else {
|
|
952
|
+
results.push({
|
|
953
|
+
name: "Cache",
|
|
954
|
+
status: "pass",
|
|
955
|
+
message: "Not yet created (will be created on first use)"
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
console.log("\n\u{1F4CB} Health Check Results:\n");
|
|
959
|
+
let hasFailures = false;
|
|
960
|
+
for (const result of results) {
|
|
961
|
+
const icon = result.status === "pass" ? "\u2705" : result.status === "warn" ? "\u26A0\uFE0F" : "\u274C";
|
|
962
|
+
console.log(`${icon} ${result.name}: ${result.message}`);
|
|
963
|
+
if (result.status === "fail") {
|
|
964
|
+
hasFailures = true;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
console.log("");
|
|
968
|
+
if (hasFailures) {
|
|
969
|
+
console.log("\u274C Some checks failed. Please fix the issues above.\n");
|
|
970
|
+
process.exit(1);
|
|
971
|
+
} else {
|
|
972
|
+
console.log("\u2705 All checks passed! Server is ready to use.\n");
|
|
973
|
+
console.log("Add to Claude Desktop config:");
|
|
974
|
+
console.log("");
|
|
975
|
+
console.log(JSON.stringify({
|
|
976
|
+
"mcpServers": {
|
|
977
|
+
"adsense": {
|
|
978
|
+
"command": "npx",
|
|
979
|
+
"args": ["-y", "adsense-mcp", "run"]
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}, null, 2));
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// src/server/index.ts
|
|
987
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
988
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
989
|
+
import {
|
|
990
|
+
CallToolRequestSchema,
|
|
991
|
+
ListToolsRequestSchema,
|
|
992
|
+
ListResourcesRequestSchema,
|
|
993
|
+
ReadResourceRequestSchema
|
|
994
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
995
|
+
|
|
996
|
+
// src/server/tools/accounts.ts
|
|
997
|
+
async function handleListAccounts() {
|
|
998
|
+
const client = await AdSenseClient.create();
|
|
999
|
+
const accounts = await client.listAccounts();
|
|
1000
|
+
return {
|
|
1001
|
+
accounts: accounts.map((account) => ({
|
|
1002
|
+
id: account.name.replace("accounts/", ""),
|
|
1003
|
+
name: account.name,
|
|
1004
|
+
displayName: account.displayName || "Unnamed Account",
|
|
1005
|
+
timeZone: account.timeZone?.id || "Unknown",
|
|
1006
|
+
createTime: account.createTime,
|
|
1007
|
+
premium: account.premium || false
|
|
1008
|
+
})),
|
|
1009
|
+
total: accounts.length
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// src/server/tools/reports.ts
|
|
1014
|
+
async function handleEarningsSummary(args) {
|
|
1015
|
+
const accountId = args.accountId || process.env.ADSENSE_ACCOUNT_ID;
|
|
1016
|
+
const client = await AdSenseClient.create(accountId);
|
|
1017
|
+
const summary = await client.getEarningsSummary(accountId);
|
|
1018
|
+
const formatCurrency = (value) => `$${value.toFixed(2)}`;
|
|
1019
|
+
const formatNumber = (value) => value.toLocaleString();
|
|
1020
|
+
const formatPercent = (value) => `${(value * 100).toFixed(2)}%`;
|
|
1021
|
+
return {
|
|
1022
|
+
summary: {
|
|
1023
|
+
today: {
|
|
1024
|
+
earnings: formatCurrency(summary.today.earnings),
|
|
1025
|
+
impressions: formatNumber(summary.today.impressions),
|
|
1026
|
+
clicks: formatNumber(summary.today.clicks),
|
|
1027
|
+
ctr: formatPercent(summary.today.ctr),
|
|
1028
|
+
rpm: formatCurrency(summary.today.rpm),
|
|
1029
|
+
pageViews: formatNumber(summary.today.pageViews)
|
|
1030
|
+
},
|
|
1031
|
+
yesterday: {
|
|
1032
|
+
earnings: formatCurrency(summary.yesterday.earnings),
|
|
1033
|
+
impressions: formatNumber(summary.yesterday.impressions),
|
|
1034
|
+
clicks: formatNumber(summary.yesterday.clicks),
|
|
1035
|
+
ctr: formatPercent(summary.yesterday.ctr),
|
|
1036
|
+
rpm: formatCurrency(summary.yesterday.rpm),
|
|
1037
|
+
pageViews: formatNumber(summary.yesterday.pageViews)
|
|
1038
|
+
},
|
|
1039
|
+
last7Days: {
|
|
1040
|
+
earnings: formatCurrency(summary.last7Days.earnings),
|
|
1041
|
+
impressions: formatNumber(summary.last7Days.impressions),
|
|
1042
|
+
clicks: formatNumber(summary.last7Days.clicks),
|
|
1043
|
+
ctr: formatPercent(summary.last7Days.ctr),
|
|
1044
|
+
rpm: formatCurrency(summary.last7Days.rpm),
|
|
1045
|
+
pageViews: formatNumber(summary.last7Days.pageViews)
|
|
1046
|
+
},
|
|
1047
|
+
thisMonth: {
|
|
1048
|
+
earnings: formatCurrency(summary.thisMonth.earnings),
|
|
1049
|
+
impressions: formatNumber(summary.thisMonth.impressions),
|
|
1050
|
+
clicks: formatNumber(summary.thisMonth.clicks),
|
|
1051
|
+
ctr: formatPercent(summary.thisMonth.ctr),
|
|
1052
|
+
rpm: formatCurrency(summary.thisMonth.rpm),
|
|
1053
|
+
pageViews: formatNumber(summary.thisMonth.pageViews)
|
|
1054
|
+
},
|
|
1055
|
+
lastMonth: {
|
|
1056
|
+
earnings: formatCurrency(summary.lastMonth.earnings),
|
|
1057
|
+
impressions: formatNumber(summary.lastMonth.impressions),
|
|
1058
|
+
clicks: formatNumber(summary.lastMonth.clicks),
|
|
1059
|
+
ctr: formatPercent(summary.lastMonth.ctr),
|
|
1060
|
+
rpm: formatCurrency(summary.lastMonth.rpm),
|
|
1061
|
+
pageViews: formatNumber(summary.lastMonth.pageViews)
|
|
1062
|
+
}
|
|
1063
|
+
},
|
|
1064
|
+
raw: summary,
|
|
1065
|
+
comparison: {
|
|
1066
|
+
vsYesterday: summary.yesterday.earnings > 0 ? ((summary.today.earnings - summary.yesterday.earnings) / summary.yesterday.earnings * 100).toFixed(1) + "%" : "N/A",
|
|
1067
|
+
vsLastMonth: summary.lastMonth.earnings > 0 ? ((summary.thisMonth.earnings - summary.lastMonth.earnings) / summary.lastMonth.earnings * 100).toFixed(1) + "%" : "N/A"
|
|
1068
|
+
}
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
async function handleGenerateReport(args) {
|
|
1072
|
+
const accountId = args.accountId || process.env.ADSENSE_ACCOUNT_ID;
|
|
1073
|
+
const client = await AdSenseClient.create(accountId);
|
|
1074
|
+
const endDate = args.endDate || getYesterday();
|
|
1075
|
+
const startDate = args.startDate || get7DaysAgo();
|
|
1076
|
+
const report = await client.generateReport({
|
|
1077
|
+
accountId: accountId || "",
|
|
1078
|
+
startDate,
|
|
1079
|
+
endDate,
|
|
1080
|
+
dimensions: args.dimensions || void 0,
|
|
1081
|
+
metrics: args.metrics || void 0,
|
|
1082
|
+
orderBy: args.orderBy || void 0,
|
|
1083
|
+
limit: args.limit || 100
|
|
1084
|
+
});
|
|
1085
|
+
const headers = report.headers?.map((h) => h.name) || [];
|
|
1086
|
+
const rows = report.rows?.map(
|
|
1087
|
+
(row) => row.cells.reduce((acc, cell, i) => {
|
|
1088
|
+
acc[headers[i]] = cell.value;
|
|
1089
|
+
return acc;
|
|
1090
|
+
}, {})
|
|
1091
|
+
) || [];
|
|
1092
|
+
return {
|
|
1093
|
+
dateRange: {
|
|
1094
|
+
start: startDate,
|
|
1095
|
+
end: endDate
|
|
1096
|
+
},
|
|
1097
|
+
headers,
|
|
1098
|
+
rows,
|
|
1099
|
+
totals: report.totals?.cells.reduce((acc, cell, i) => {
|
|
1100
|
+
acc[headers[i]] = cell.value;
|
|
1101
|
+
return acc;
|
|
1102
|
+
}, {}),
|
|
1103
|
+
totalRows: report.totalMatchedRows || rows.length.toString()
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
async function handleComparePeriods(args) {
|
|
1107
|
+
const accountId = args.accountId || process.env.ADSENSE_ACCOUNT_ID;
|
|
1108
|
+
const client = await AdSenseClient.create(accountId);
|
|
1109
|
+
const period1Start = args.period1Start;
|
|
1110
|
+
const period1End = args.period1End;
|
|
1111
|
+
const period2Start = args.period2Start;
|
|
1112
|
+
const period2End = args.period2End;
|
|
1113
|
+
if (!period1Start || !period1End || !period2Start || !period2End) {
|
|
1114
|
+
throw new Error("All period dates are required");
|
|
1115
|
+
}
|
|
1116
|
+
const dimensions = args.dimensions || void 0;
|
|
1117
|
+
const [report1, report2] = await Promise.all([
|
|
1118
|
+
client.generateReport({
|
|
1119
|
+
accountId: accountId || "",
|
|
1120
|
+
startDate: period1Start,
|
|
1121
|
+
endDate: period1End,
|
|
1122
|
+
dimensions
|
|
1123
|
+
}),
|
|
1124
|
+
client.generateReport({
|
|
1125
|
+
accountId: accountId || "",
|
|
1126
|
+
startDate: period2Start,
|
|
1127
|
+
endDate: period2End,
|
|
1128
|
+
dimensions
|
|
1129
|
+
})
|
|
1130
|
+
]);
|
|
1131
|
+
const extractTotals = (report) => {
|
|
1132
|
+
const headers = report.headers?.map((h) => h.name) || [];
|
|
1133
|
+
const totals = report.totals?.cells || report.rows?.[0]?.cells || [];
|
|
1134
|
+
return headers.reduce((acc, header, i) => {
|
|
1135
|
+
acc[header] = parseFloat(totals[i]?.value) || 0;
|
|
1136
|
+
return acc;
|
|
1137
|
+
}, {});
|
|
1138
|
+
};
|
|
1139
|
+
const period1Totals = extractTotals(report1);
|
|
1140
|
+
const period2Totals = extractTotals(report2);
|
|
1141
|
+
const changes = {};
|
|
1142
|
+
for (const key of Object.keys(period1Totals)) {
|
|
1143
|
+
const val1 = period1Totals[key];
|
|
1144
|
+
const val2 = period2Totals[key] || 0;
|
|
1145
|
+
const diff = val1 - val2;
|
|
1146
|
+
const percent = val2 !== 0 ? diff / val2 * 100 : 0;
|
|
1147
|
+
changes[key] = {
|
|
1148
|
+
period1: val1,
|
|
1149
|
+
period2: val2,
|
|
1150
|
+
change: diff >= 0 ? `+${diff.toFixed(2)}` : diff.toFixed(2),
|
|
1151
|
+
changePercent: percent >= 0 ? `+${percent.toFixed(1)}%` : `${percent.toFixed(1)}%`
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
return {
|
|
1155
|
+
period1: {
|
|
1156
|
+
start: period1Start,
|
|
1157
|
+
end: period1End,
|
|
1158
|
+
totals: period1Totals
|
|
1159
|
+
},
|
|
1160
|
+
period2: {
|
|
1161
|
+
start: period2Start,
|
|
1162
|
+
end: period2End,
|
|
1163
|
+
totals: period2Totals
|
|
1164
|
+
},
|
|
1165
|
+
changes
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
function getYesterday() {
|
|
1169
|
+
const d = /* @__PURE__ */ new Date();
|
|
1170
|
+
d.setDate(d.getDate() - 1);
|
|
1171
|
+
return d.toISOString().split("T")[0];
|
|
1172
|
+
}
|
|
1173
|
+
function get7DaysAgo() {
|
|
1174
|
+
const d = /* @__PURE__ */ new Date();
|
|
1175
|
+
d.setDate(d.getDate() - 7);
|
|
1176
|
+
return d.toISOString().split("T")[0];
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// src/server/tools/sites.ts
|
|
1180
|
+
async function handleListSites(args) {
|
|
1181
|
+
const accountId = args.accountId || process.env.ADSENSE_ACCOUNT_ID;
|
|
1182
|
+
const client = await AdSenseClient.create(accountId);
|
|
1183
|
+
const sites = await client.listSites(accountId);
|
|
1184
|
+
const byStatus = {
|
|
1185
|
+
ready: sites.filter((s) => s.state === "READY"),
|
|
1186
|
+
gettingReady: sites.filter((s) => s.state === "GETTING_READY"),
|
|
1187
|
+
needsAttention: sites.filter((s) => s.state === "NEEDS_ATTENTION"),
|
|
1188
|
+
requiresReview: sites.filter((s) => s.state === "REQUIRES_REVIEW")
|
|
1189
|
+
};
|
|
1190
|
+
const formatSite = (site) => {
|
|
1191
|
+
const statusEmojiMap = {
|
|
1192
|
+
"READY": "\u2705",
|
|
1193
|
+
"GETTING_READY": "\u23F3",
|
|
1194
|
+
"NEEDS_ATTENTION": "\u26A0\uFE0F",
|
|
1195
|
+
"REQUIRES_REVIEW": "\u{1F4DD}",
|
|
1196
|
+
"STATE_UNSPECIFIED": "\u2753"
|
|
1197
|
+
};
|
|
1198
|
+
const statusEmoji = statusEmojiMap[site.state] || "\u2753";
|
|
1199
|
+
return {
|
|
1200
|
+
domain: site.domain,
|
|
1201
|
+
status: site.state,
|
|
1202
|
+
statusEmoji,
|
|
1203
|
+
autoAdsEnabled: site.autoAdsEnabled || false,
|
|
1204
|
+
name: site.name
|
|
1205
|
+
};
|
|
1206
|
+
};
|
|
1207
|
+
return {
|
|
1208
|
+
sites: sites.map(formatSite),
|
|
1209
|
+
summary: {
|
|
1210
|
+
total: sites.length,
|
|
1211
|
+
ready: byStatus.ready.length,
|
|
1212
|
+
gettingReady: byStatus.gettingReady.length,
|
|
1213
|
+
needsAttention: byStatus.needsAttention.length,
|
|
1214
|
+
requiresReview: byStatus.requiresReview.length
|
|
1215
|
+
},
|
|
1216
|
+
statusDescriptions: {
|
|
1217
|
+
READY: "Approved and serving ads",
|
|
1218
|
+
GETTING_READY: "Under review by Google (may take 1-2 weeks)",
|
|
1219
|
+
NEEDS_ATTENTION: "Issues to fix before approval",
|
|
1220
|
+
REQUIRES_REVIEW: "Site needs review or is inactive"
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// src/server/tools/alerts.ts
|
|
1226
|
+
async function handleListAlerts(args) {
|
|
1227
|
+
const accountId = args.accountId || process.env.ADSENSE_ACCOUNT_ID;
|
|
1228
|
+
const client = await AdSenseClient.create(accountId);
|
|
1229
|
+
const alerts = await client.listAlerts(accountId);
|
|
1230
|
+
const bySeverity = {
|
|
1231
|
+
severe: alerts.filter((a) => a.severity === "SEVERE"),
|
|
1232
|
+
warning: alerts.filter((a) => a.severity === "WARNING"),
|
|
1233
|
+
info: alerts.filter((a) => a.severity === "INFO")
|
|
1234
|
+
};
|
|
1235
|
+
const formatAlert = (alert) => {
|
|
1236
|
+
const severityEmojiMap = {
|
|
1237
|
+
"SEVERE": "\u{1F534}",
|
|
1238
|
+
"WARNING": "\u{1F7E1}",
|
|
1239
|
+
"INFO": "\u{1F535}",
|
|
1240
|
+
"SEVERITY_UNSPECIFIED": "\u26AA"
|
|
1241
|
+
};
|
|
1242
|
+
const severityEmoji = severityEmojiMap[alert.severity] || "\u26AA";
|
|
1243
|
+
return {
|
|
1244
|
+
message: alert.message,
|
|
1245
|
+
severity: alert.severity,
|
|
1246
|
+
severityEmoji,
|
|
1247
|
+
type: alert.type,
|
|
1248
|
+
name: alert.name
|
|
1249
|
+
};
|
|
1250
|
+
};
|
|
1251
|
+
return {
|
|
1252
|
+
alerts: alerts.map(formatAlert),
|
|
1253
|
+
summary: {
|
|
1254
|
+
total: alerts.length,
|
|
1255
|
+
severe: bySeverity.severe.length,
|
|
1256
|
+
warning: bySeverity.warning.length,
|
|
1257
|
+
info: bySeverity.info.length
|
|
1258
|
+
},
|
|
1259
|
+
hasIssues: bySeverity.severe.length > 0 || bySeverity.warning.length > 0,
|
|
1260
|
+
severityDescriptions: {
|
|
1261
|
+
SEVERE: "Immediate action required (payment holds, violations)",
|
|
1262
|
+
WARNING: "Action recommended",
|
|
1263
|
+
INFO: "General notifications"
|
|
1264
|
+
}
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
async function handleListPolicyIssues(args) {
|
|
1268
|
+
const accountId = args.accountId || process.env.ADSENSE_ACCOUNT_ID;
|
|
1269
|
+
const client = await AdSenseClient.create(accountId);
|
|
1270
|
+
const issues = await client.listPolicyIssues(accountId);
|
|
1271
|
+
const formatIssue = (issue) => {
|
|
1272
|
+
const actionEmojiMap = {
|
|
1273
|
+
"WARNED": "\u26A0\uFE0F",
|
|
1274
|
+
"AD_SERVING_RESTRICTED": "\u{1F536}",
|
|
1275
|
+
"AD_SERVING_DISABLED": "\u{1F534}",
|
|
1276
|
+
"AD_SERVED_WITH_CLICK_CONFIRMATION": "\u{1F7E1}",
|
|
1277
|
+
"AD_PERSONALIZATION_RESTRICTED": "\u{1F7E0}",
|
|
1278
|
+
"ENFORCEMENT_ACTION_UNSPECIFIED": "\u26AA"
|
|
1279
|
+
};
|
|
1280
|
+
const actionEmoji = actionEmojiMap[issue.action] || "\u26AA";
|
|
1281
|
+
return {
|
|
1282
|
+
site: issue.site,
|
|
1283
|
+
siteSection: issue.siteSection,
|
|
1284
|
+
uri: issue.uri,
|
|
1285
|
+
entityType: issue.entityType,
|
|
1286
|
+
action: issue.action,
|
|
1287
|
+
actionEmoji,
|
|
1288
|
+
adRequestCount: issue.adRequestCount,
|
|
1289
|
+
warningEscalationDate: issue.warningEscalationDate ? `${issue.warningEscalationDate.year}-${String(issue.warningEscalationDate.month).padStart(2, "0")}-${String(issue.warningEscalationDate.day).padStart(2, "0")}` : null,
|
|
1290
|
+
name: issue.name
|
|
1291
|
+
};
|
|
1292
|
+
};
|
|
1293
|
+
const byAction = {
|
|
1294
|
+
warned: issues.filter((i) => i.action === "WARNED"),
|
|
1295
|
+
restricted: issues.filter((i) => i.action === "AD_SERVING_RESTRICTED"),
|
|
1296
|
+
disabled: issues.filter((i) => i.action === "AD_SERVING_DISABLED"),
|
|
1297
|
+
clickConfirmation: issues.filter((i) => i.action === "AD_SERVED_WITH_CLICK_CONFIRMATION"),
|
|
1298
|
+
personalizationRestricted: issues.filter((i) => i.action === "AD_PERSONALIZATION_RESTRICTED")
|
|
1299
|
+
};
|
|
1300
|
+
return {
|
|
1301
|
+
issues: issues.map(formatIssue),
|
|
1302
|
+
summary: {
|
|
1303
|
+
total: issues.length,
|
|
1304
|
+
warned: byAction.warned.length,
|
|
1305
|
+
restricted: byAction.restricted.length,
|
|
1306
|
+
disabled: byAction.disabled.length,
|
|
1307
|
+
clickConfirmation: byAction.clickConfirmation.length,
|
|
1308
|
+
personalizationRestricted: byAction.personalizationRestricted.length
|
|
1309
|
+
},
|
|
1310
|
+
hasIssues: issues.length > 0,
|
|
1311
|
+
actionDescriptions: {
|
|
1312
|
+
WARNED: "Pending enforcement with deadline - fix before escalation",
|
|
1313
|
+
AD_SERVING_RESTRICTED: "Reduced ad demand on affected pages",
|
|
1314
|
+
AD_SERVING_DISABLED: "Ads completely stopped on affected pages",
|
|
1315
|
+
AD_SERVED_WITH_CLICK_CONFIRMATION: "Extra click verification required",
|
|
1316
|
+
AD_PERSONALIZATION_RESTRICTED: "Limited to basic (non-personalized) ads"
|
|
1317
|
+
}
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// src/server/tools/payments.ts
|
|
1322
|
+
async function handleListPayments(args) {
|
|
1323
|
+
const accountId = args.accountId || process.env.ADSENSE_ACCOUNT_ID;
|
|
1324
|
+
const client = await AdSenseClient.create(accountId);
|
|
1325
|
+
const payments = await client.listPayments(accountId);
|
|
1326
|
+
const formatPayment = (payment) => {
|
|
1327
|
+
const date = payment.date ? `${payment.date.year}-${String(payment.date.month).padStart(2, "0")}-${String(payment.date.day).padStart(2, "0")}` : null;
|
|
1328
|
+
const name = payment.name || "";
|
|
1329
|
+
let type = "payment";
|
|
1330
|
+
if (name.includes("unpaid")) {
|
|
1331
|
+
type = "unpaid";
|
|
1332
|
+
} else if (name.includes("youtube")) {
|
|
1333
|
+
type = "youtube";
|
|
1334
|
+
}
|
|
1335
|
+
return {
|
|
1336
|
+
amount: payment.amount || "N/A",
|
|
1337
|
+
date,
|
|
1338
|
+
type,
|
|
1339
|
+
name: payment.name
|
|
1340
|
+
};
|
|
1341
|
+
};
|
|
1342
|
+
const unpaid = payments.filter((p) => p.name?.includes("unpaid"));
|
|
1343
|
+
const paid = payments.filter((p) => !p.name?.includes("unpaid"));
|
|
1344
|
+
const totalPaid = paid.reduce((sum, p) => {
|
|
1345
|
+
const match = p.amount?.match(/[\d.]+/);
|
|
1346
|
+
return sum + (match ? parseFloat(match[0]) : 0);
|
|
1347
|
+
}, 0);
|
|
1348
|
+
return {
|
|
1349
|
+
payments: payments.map(formatPayment),
|
|
1350
|
+
summary: {
|
|
1351
|
+
totalPayments: paid.length,
|
|
1352
|
+
unpaidBalance: unpaid.length > 0 ? unpaid[0].amount : "N/A",
|
|
1353
|
+
totalPaidAllTime: `$${totalPaid.toFixed(2)}`
|
|
1354
|
+
},
|
|
1355
|
+
unpaid: unpaid.map(formatPayment),
|
|
1356
|
+
paid: paid.map(formatPayment)
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// src/server/tools/adUnits.ts
|
|
1361
|
+
async function handleListAdUnits(args) {
|
|
1362
|
+
const accountId = args.accountId || process.env.ADSENSE_ACCOUNT_ID;
|
|
1363
|
+
const client = await AdSenseClient.create(accountId);
|
|
1364
|
+
const adUnits = await client.listAdUnits(accountId);
|
|
1365
|
+
const formatAdUnit = (unit) => {
|
|
1366
|
+
const stateEmojiMap = {
|
|
1367
|
+
"ACTIVE": "\u2705",
|
|
1368
|
+
"ARCHIVED": "\u{1F4E6}",
|
|
1369
|
+
"STATE_UNSPECIFIED": "\u2753"
|
|
1370
|
+
};
|
|
1371
|
+
const stateEmoji = stateEmojiMap[unit.state] || "\u2753";
|
|
1372
|
+
const nameParts = unit.name.split("/");
|
|
1373
|
+
const adClientId = nameParts[3] || "";
|
|
1374
|
+
const adUnitId = nameParts[5] || "";
|
|
1375
|
+
return {
|
|
1376
|
+
displayName: unit.displayName,
|
|
1377
|
+
state: unit.state,
|
|
1378
|
+
stateEmoji,
|
|
1379
|
+
type: unit.contentAdsSettings?.type || "UNKNOWN",
|
|
1380
|
+
size: unit.contentAdsSettings?.size || "Auto",
|
|
1381
|
+
adClientId,
|
|
1382
|
+
adUnitId,
|
|
1383
|
+
name: unit.name,
|
|
1384
|
+
reportingDimensionId: unit.reportingDimensionId
|
|
1385
|
+
};
|
|
1386
|
+
};
|
|
1387
|
+
const byState = {
|
|
1388
|
+
active: adUnits.filter((u) => u.state === "ACTIVE"),
|
|
1389
|
+
archived: adUnits.filter((u) => u.state === "ARCHIVED")
|
|
1390
|
+
};
|
|
1391
|
+
const byType = {};
|
|
1392
|
+
for (const unit of adUnits) {
|
|
1393
|
+
const type = unit.contentAdsSettings?.type || "UNKNOWN";
|
|
1394
|
+
if (!byType[type]) {
|
|
1395
|
+
byType[type] = [];
|
|
1396
|
+
}
|
|
1397
|
+
byType[type].push(unit);
|
|
1398
|
+
}
|
|
1399
|
+
return {
|
|
1400
|
+
adUnits: adUnits.map(formatAdUnit),
|
|
1401
|
+
summary: {
|
|
1402
|
+
total: adUnits.length,
|
|
1403
|
+
active: byState.active.length,
|
|
1404
|
+
archived: byState.archived.length,
|
|
1405
|
+
byType: Object.fromEntries(
|
|
1406
|
+
Object.entries(byType).map(([type, units]) => [type, units.length])
|
|
1407
|
+
)
|
|
1408
|
+
},
|
|
1409
|
+
typeDescriptions: {
|
|
1410
|
+
DISPLAY: "Standard display ads (image, text, rich media)",
|
|
1411
|
+
FEED: "In-feed ads that match your content style",
|
|
1412
|
+
ARTICLE: "In-article ads for between paragraphs",
|
|
1413
|
+
MATCHED_CONTENT: "Content recommendations with ads",
|
|
1414
|
+
LINK: "Contextual link unit ads"
|
|
1415
|
+
}
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
async function handleGetAdCode(args) {
|
|
1419
|
+
const accountId = args.accountId || process.env.ADSENSE_ACCOUNT_ID;
|
|
1420
|
+
const adClientId = args.adClientId;
|
|
1421
|
+
const adUnitId = args.adUnitId;
|
|
1422
|
+
if (!adClientId || !adUnitId) {
|
|
1423
|
+
throw new Error("adClientId and adUnitId are required");
|
|
1424
|
+
}
|
|
1425
|
+
const client = await AdSenseClient.create(accountId);
|
|
1426
|
+
const adCode = await client.getAdCode(adClientId, adUnitId, accountId);
|
|
1427
|
+
return {
|
|
1428
|
+
adCode,
|
|
1429
|
+
adClientId,
|
|
1430
|
+
adUnitId,
|
|
1431
|
+
instructions: [
|
|
1432
|
+
"Copy the ad code above",
|
|
1433
|
+
"Paste it into your website HTML where you want the ad to appear",
|
|
1434
|
+
"The ad code should be placed within the <body> tags",
|
|
1435
|
+
"Make sure to test the ad placement before publishing"
|
|
1436
|
+
]
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
// src/server/tools/export.ts
|
|
1441
|
+
async function handleExportCsv(args) {
|
|
1442
|
+
const accountId = args.accountId || process.env.ADSENSE_ACCOUNT_ID;
|
|
1443
|
+
const startDate = args.startDate;
|
|
1444
|
+
const endDate = args.endDate;
|
|
1445
|
+
if (!startDate || !endDate) {
|
|
1446
|
+
throw new Error("startDate and endDate are required");
|
|
1447
|
+
}
|
|
1448
|
+
const client = await AdSenseClient.create(accountId);
|
|
1449
|
+
try {
|
|
1450
|
+
const csvData = await client.generateCsvReport({
|
|
1451
|
+
accountId: accountId || "",
|
|
1452
|
+
startDate,
|
|
1453
|
+
endDate,
|
|
1454
|
+
dimensions: args.dimensions || void 0,
|
|
1455
|
+
metrics: args.metrics || void 0
|
|
1456
|
+
});
|
|
1457
|
+
return {
|
|
1458
|
+
format: "csv",
|
|
1459
|
+
dateRange: { start: startDate, end: endDate },
|
|
1460
|
+
data: csvData
|
|
1461
|
+
};
|
|
1462
|
+
} catch {
|
|
1463
|
+
const report = await client.generateReport({
|
|
1464
|
+
accountId: accountId || "",
|
|
1465
|
+
startDate,
|
|
1466
|
+
endDate,
|
|
1467
|
+
dimensions: args.dimensions || void 0,
|
|
1468
|
+
metrics: args.metrics || void 0,
|
|
1469
|
+
limit: 1e4
|
|
1470
|
+
});
|
|
1471
|
+
const headers = report.headers?.map((h) => h.name) || [];
|
|
1472
|
+
const rows = report.rows || [];
|
|
1473
|
+
const csvLines = [
|
|
1474
|
+
headers.join(","),
|
|
1475
|
+
...rows.map(
|
|
1476
|
+
(row) => row.cells.map((cell) => {
|
|
1477
|
+
const value = cell.value;
|
|
1478
|
+
if (value.includes(",") || value.includes('"')) {
|
|
1479
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
1480
|
+
}
|
|
1481
|
+
return value;
|
|
1482
|
+
}).join(",")
|
|
1483
|
+
)
|
|
1484
|
+
];
|
|
1485
|
+
return {
|
|
1486
|
+
format: "csv",
|
|
1487
|
+
dateRange: { start: startDate, end: endDate },
|
|
1488
|
+
data: csvLines.join("\n"),
|
|
1489
|
+
rowCount: rows.length
|
|
1490
|
+
};
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
// src/server/tools/index.ts
|
|
1495
|
+
function registerTools() {
|
|
1496
|
+
return [
|
|
1497
|
+
// Account tools
|
|
1498
|
+
{
|
|
1499
|
+
name: "adsense_list_accounts",
|
|
1500
|
+
description: "List all AdSense accounts you have access to",
|
|
1501
|
+
inputSchema: {
|
|
1502
|
+
type: "object",
|
|
1503
|
+
properties: {},
|
|
1504
|
+
required: []
|
|
1505
|
+
}
|
|
1506
|
+
},
|
|
1507
|
+
// Report tools
|
|
1508
|
+
{
|
|
1509
|
+
name: "adsense_earnings_summary",
|
|
1510
|
+
description: "Get a quick earnings summary (today, yesterday, last 7 days, this month, last month)",
|
|
1511
|
+
inputSchema: {
|
|
1512
|
+
type: "object",
|
|
1513
|
+
properties: {
|
|
1514
|
+
accountId: {
|
|
1515
|
+
type: "string",
|
|
1516
|
+
description: "AdSense account ID (e.g., pub-1234567890123456). Uses default if not specified."
|
|
1517
|
+
}
|
|
1518
|
+
},
|
|
1519
|
+
required: []
|
|
1520
|
+
}
|
|
1521
|
+
},
|
|
1522
|
+
{
|
|
1523
|
+
name: "adsense_generate_report",
|
|
1524
|
+
description: "Generate a detailed AdSense performance report with custom dimensions and metrics",
|
|
1525
|
+
inputSchema: {
|
|
1526
|
+
type: "object",
|
|
1527
|
+
properties: {
|
|
1528
|
+
accountId: {
|
|
1529
|
+
type: "string",
|
|
1530
|
+
description: "AdSense account ID. Uses default if not specified."
|
|
1531
|
+
},
|
|
1532
|
+
startDate: {
|
|
1533
|
+
type: "string",
|
|
1534
|
+
description: "Start date (YYYY-MM-DD). Defaults to 7 days ago."
|
|
1535
|
+
},
|
|
1536
|
+
endDate: {
|
|
1537
|
+
type: "string",
|
|
1538
|
+
description: "End date (YYYY-MM-DD). Defaults to yesterday."
|
|
1539
|
+
},
|
|
1540
|
+
dimensions: {
|
|
1541
|
+
type: "array",
|
|
1542
|
+
items: { type: "string" },
|
|
1543
|
+
description: "Dimensions to group by: DATE, WEEK, MONTH, DOMAIN_NAME, PAGE_URL, AD_UNIT_NAME, COUNTRY_NAME, PLATFORM_TYPE_CODE, TRAFFIC_SOURCE_CODE"
|
|
1544
|
+
},
|
|
1545
|
+
metrics: {
|
|
1546
|
+
type: "array",
|
|
1547
|
+
items: { type: "string" },
|
|
1548
|
+
description: "Metrics to include: ESTIMATED_EARNINGS, IMPRESSIONS, CLICKS, PAGE_VIEWS, PAGE_VIEWS_CTR, PAGE_VIEWS_RPM, AD_REQUESTS, AD_REQUESTS_COVERAGE"
|
|
1549
|
+
},
|
|
1550
|
+
orderBy: {
|
|
1551
|
+
type: "string",
|
|
1552
|
+
description: "Metric to sort by (prefix with - for descending, e.g., -ESTIMATED_EARNINGS)"
|
|
1553
|
+
},
|
|
1554
|
+
limit: {
|
|
1555
|
+
type: "number",
|
|
1556
|
+
description: "Max rows to return (default: 100, max: 10000)"
|
|
1557
|
+
}
|
|
1558
|
+
},
|
|
1559
|
+
required: []
|
|
1560
|
+
}
|
|
1561
|
+
},
|
|
1562
|
+
{
|
|
1563
|
+
name: "adsense_compare_periods",
|
|
1564
|
+
description: "Compare AdSense performance between two time periods",
|
|
1565
|
+
inputSchema: {
|
|
1566
|
+
type: "object",
|
|
1567
|
+
properties: {
|
|
1568
|
+
accountId: {
|
|
1569
|
+
type: "string",
|
|
1570
|
+
description: "AdSense account ID. Uses default if not specified."
|
|
1571
|
+
},
|
|
1572
|
+
period1Start: {
|
|
1573
|
+
type: "string",
|
|
1574
|
+
description: "First period start date (YYYY-MM-DD)"
|
|
1575
|
+
},
|
|
1576
|
+
period1End: {
|
|
1577
|
+
type: "string",
|
|
1578
|
+
description: "First period end date (YYYY-MM-DD)"
|
|
1579
|
+
},
|
|
1580
|
+
period2Start: {
|
|
1581
|
+
type: "string",
|
|
1582
|
+
description: "Second period start date (YYYY-MM-DD)"
|
|
1583
|
+
},
|
|
1584
|
+
period2End: {
|
|
1585
|
+
type: "string",
|
|
1586
|
+
description: "Second period end date (YYYY-MM-DD)"
|
|
1587
|
+
},
|
|
1588
|
+
dimensions: {
|
|
1589
|
+
type: "array",
|
|
1590
|
+
items: { type: "string" },
|
|
1591
|
+
description: "Dimensions to group by for comparison"
|
|
1592
|
+
}
|
|
1593
|
+
},
|
|
1594
|
+
required: ["period1Start", "period1End", "period2Start", "period2End"]
|
|
1595
|
+
}
|
|
1596
|
+
},
|
|
1597
|
+
// Sites tools
|
|
1598
|
+
{
|
|
1599
|
+
name: "adsense_list_sites",
|
|
1600
|
+
description: "List all sites and their AdSense approval status (READY, GETTING_READY, NEEDS_ATTENTION, REQUIRES_REVIEW)",
|
|
1601
|
+
inputSchema: {
|
|
1602
|
+
type: "object",
|
|
1603
|
+
properties: {
|
|
1604
|
+
accountId: {
|
|
1605
|
+
type: "string",
|
|
1606
|
+
description: "AdSense account ID. Uses default if not specified."
|
|
1607
|
+
}
|
|
1608
|
+
},
|
|
1609
|
+
required: []
|
|
1610
|
+
}
|
|
1611
|
+
},
|
|
1612
|
+
// Alerts tools
|
|
1613
|
+
{
|
|
1614
|
+
name: "adsense_list_alerts",
|
|
1615
|
+
description: "List AdSense account alerts and warnings (INFO, WARNING, SEVERE)",
|
|
1616
|
+
inputSchema: {
|
|
1617
|
+
type: "object",
|
|
1618
|
+
properties: {
|
|
1619
|
+
accountId: {
|
|
1620
|
+
type: "string",
|
|
1621
|
+
description: "AdSense account ID. Uses default if not specified."
|
|
1622
|
+
}
|
|
1623
|
+
},
|
|
1624
|
+
required: []
|
|
1625
|
+
}
|
|
1626
|
+
},
|
|
1627
|
+
{
|
|
1628
|
+
name: "adsense_list_policy_issues",
|
|
1629
|
+
description: "List policy issues and violations affecting your account (WARNED, AD_SERVING_RESTRICTED, AD_SERVING_DISABLED)",
|
|
1630
|
+
inputSchema: {
|
|
1631
|
+
type: "object",
|
|
1632
|
+
properties: {
|
|
1633
|
+
accountId: {
|
|
1634
|
+
type: "string",
|
|
1635
|
+
description: "AdSense account ID. Uses default if not specified."
|
|
1636
|
+
}
|
|
1637
|
+
},
|
|
1638
|
+
required: []
|
|
1639
|
+
}
|
|
1640
|
+
},
|
|
1641
|
+
// Payments tools
|
|
1642
|
+
{
|
|
1643
|
+
name: "adsense_list_payments",
|
|
1644
|
+
description: "List payment history and pending earnings",
|
|
1645
|
+
inputSchema: {
|
|
1646
|
+
type: "object",
|
|
1647
|
+
properties: {
|
|
1648
|
+
accountId: {
|
|
1649
|
+
type: "string",
|
|
1650
|
+
description: "AdSense account ID. Uses default if not specified."
|
|
1651
|
+
}
|
|
1652
|
+
},
|
|
1653
|
+
required: []
|
|
1654
|
+
}
|
|
1655
|
+
},
|
|
1656
|
+
// Ad units tools
|
|
1657
|
+
{
|
|
1658
|
+
name: "adsense_list_ad_units",
|
|
1659
|
+
description: "List all ad units across your ad clients",
|
|
1660
|
+
inputSchema: {
|
|
1661
|
+
type: "object",
|
|
1662
|
+
properties: {
|
|
1663
|
+
accountId: {
|
|
1664
|
+
type: "string",
|
|
1665
|
+
description: "AdSense account ID. Uses default if not specified."
|
|
1666
|
+
}
|
|
1667
|
+
},
|
|
1668
|
+
required: []
|
|
1669
|
+
}
|
|
1670
|
+
},
|
|
1671
|
+
{
|
|
1672
|
+
name: "adsense_get_ad_code",
|
|
1673
|
+
description: "Get the HTML embed code for an ad unit",
|
|
1674
|
+
inputSchema: {
|
|
1675
|
+
type: "object",
|
|
1676
|
+
properties: {
|
|
1677
|
+
accountId: {
|
|
1678
|
+
type: "string",
|
|
1679
|
+
description: "AdSense account ID. Uses default if not specified."
|
|
1680
|
+
},
|
|
1681
|
+
adClientId: {
|
|
1682
|
+
type: "string",
|
|
1683
|
+
description: "Ad client ID (e.g., ca-pub-1234567890123456)"
|
|
1684
|
+
},
|
|
1685
|
+
adUnitId: {
|
|
1686
|
+
type: "string",
|
|
1687
|
+
description: "Ad unit ID"
|
|
1688
|
+
}
|
|
1689
|
+
},
|
|
1690
|
+
required: ["adClientId", "adUnitId"]
|
|
1691
|
+
}
|
|
1692
|
+
},
|
|
1693
|
+
// Export tools
|
|
1694
|
+
{
|
|
1695
|
+
name: "adsense_export_csv",
|
|
1696
|
+
description: "Export AdSense report data as CSV format",
|
|
1697
|
+
inputSchema: {
|
|
1698
|
+
type: "object",
|
|
1699
|
+
properties: {
|
|
1700
|
+
accountId: {
|
|
1701
|
+
type: "string",
|
|
1702
|
+
description: "AdSense account ID. Uses default if not specified."
|
|
1703
|
+
},
|
|
1704
|
+
startDate: {
|
|
1705
|
+
type: "string",
|
|
1706
|
+
description: "Start date (YYYY-MM-DD)"
|
|
1707
|
+
},
|
|
1708
|
+
endDate: {
|
|
1709
|
+
type: "string",
|
|
1710
|
+
description: "End date (YYYY-MM-DD)"
|
|
1711
|
+
},
|
|
1712
|
+
dimensions: {
|
|
1713
|
+
type: "array",
|
|
1714
|
+
items: { type: "string" },
|
|
1715
|
+
description: "Dimensions to include"
|
|
1716
|
+
},
|
|
1717
|
+
metrics: {
|
|
1718
|
+
type: "array",
|
|
1719
|
+
items: { type: "string" },
|
|
1720
|
+
description: "Metrics to include"
|
|
1721
|
+
}
|
|
1722
|
+
},
|
|
1723
|
+
required: ["startDate", "endDate"]
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
];
|
|
1727
|
+
}
|
|
1728
|
+
async function handleToolCall(name, args) {
|
|
1729
|
+
try {
|
|
1730
|
+
let result;
|
|
1731
|
+
switch (name) {
|
|
1732
|
+
case "adsense_list_accounts":
|
|
1733
|
+
result = await handleListAccounts();
|
|
1734
|
+
break;
|
|
1735
|
+
case "adsense_earnings_summary":
|
|
1736
|
+
result = await handleEarningsSummary(args);
|
|
1737
|
+
break;
|
|
1738
|
+
case "adsense_generate_report":
|
|
1739
|
+
result = await handleGenerateReport(args);
|
|
1740
|
+
break;
|
|
1741
|
+
case "adsense_compare_periods":
|
|
1742
|
+
result = await handleComparePeriods(args);
|
|
1743
|
+
break;
|
|
1744
|
+
case "adsense_list_sites":
|
|
1745
|
+
result = await handleListSites(args);
|
|
1746
|
+
break;
|
|
1747
|
+
case "adsense_list_alerts":
|
|
1748
|
+
result = await handleListAlerts(args);
|
|
1749
|
+
break;
|
|
1750
|
+
case "adsense_list_policy_issues":
|
|
1751
|
+
result = await handleListPolicyIssues(args);
|
|
1752
|
+
break;
|
|
1753
|
+
case "adsense_list_payments":
|
|
1754
|
+
result = await handleListPayments(args);
|
|
1755
|
+
break;
|
|
1756
|
+
case "adsense_list_ad_units":
|
|
1757
|
+
result = await handleListAdUnits(args);
|
|
1758
|
+
break;
|
|
1759
|
+
case "adsense_get_ad_code":
|
|
1760
|
+
result = await handleGetAdCode(args);
|
|
1761
|
+
break;
|
|
1762
|
+
case "adsense_export_csv":
|
|
1763
|
+
result = await handleExportCsv(args);
|
|
1764
|
+
break;
|
|
1765
|
+
default:
|
|
1766
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
1767
|
+
}
|
|
1768
|
+
return {
|
|
1769
|
+
content: [
|
|
1770
|
+
{
|
|
1771
|
+
type: "text",
|
|
1772
|
+
text: typeof result === "string" ? result : JSON.stringify(result, null, 2)
|
|
1773
|
+
}
|
|
1774
|
+
]
|
|
1775
|
+
};
|
|
1776
|
+
} catch (error) {
|
|
1777
|
+
return {
|
|
1778
|
+
content: [
|
|
1779
|
+
{
|
|
1780
|
+
type: "text",
|
|
1781
|
+
text: `Error: ${error.message}`
|
|
1782
|
+
}
|
|
1783
|
+
]
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
// src/server/resources/index.ts
|
|
1789
|
+
async function handleListResources() {
|
|
1790
|
+
return {
|
|
1791
|
+
resources: [
|
|
1792
|
+
{
|
|
1793
|
+
uri: "adsense://accounts",
|
|
1794
|
+
name: "AdSense Accounts",
|
|
1795
|
+
description: "List of all AdSense accounts you have access to",
|
|
1796
|
+
mimeType: "application/json"
|
|
1797
|
+
}
|
|
1798
|
+
]
|
|
1799
|
+
};
|
|
1800
|
+
}
|
|
1801
|
+
async function handleReadResource(uri) {
|
|
1802
|
+
const url = new URL(uri);
|
|
1803
|
+
if (url.protocol !== "adsense:") {
|
|
1804
|
+
throw new Error(`Unsupported protocol: ${url.protocol}`);
|
|
1805
|
+
}
|
|
1806
|
+
const path4 = url.pathname.replace(/^\/\//, "");
|
|
1807
|
+
if (path4 === "accounts") {
|
|
1808
|
+
const client = await AdSenseClient.create();
|
|
1809
|
+
const accounts = await client.listAccounts();
|
|
1810
|
+
return {
|
|
1811
|
+
contents: [
|
|
1812
|
+
{
|
|
1813
|
+
uri,
|
|
1814
|
+
mimeType: "application/json",
|
|
1815
|
+
text: JSON.stringify(accounts, null, 2)
|
|
1816
|
+
}
|
|
1817
|
+
]
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
const sitesMatch = path4.match(/^accounts\/([^/]+)\/sites$/);
|
|
1821
|
+
if (sitesMatch) {
|
|
1822
|
+
const accountId = sitesMatch[1];
|
|
1823
|
+
const client = await AdSenseClient.create(accountId);
|
|
1824
|
+
const sites = await client.listSites(accountId);
|
|
1825
|
+
return {
|
|
1826
|
+
contents: [
|
|
1827
|
+
{
|
|
1828
|
+
uri,
|
|
1829
|
+
mimeType: "application/json",
|
|
1830
|
+
text: JSON.stringify(sites, null, 2)
|
|
1831
|
+
}
|
|
1832
|
+
]
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1835
|
+
const adUnitsMatch = path4.match(/^accounts\/([^/]+)\/adunits$/);
|
|
1836
|
+
if (adUnitsMatch) {
|
|
1837
|
+
const accountId = adUnitsMatch[1];
|
|
1838
|
+
const client = await AdSenseClient.create(accountId);
|
|
1839
|
+
const adUnits = await client.listAdUnits(accountId);
|
|
1840
|
+
return {
|
|
1841
|
+
contents: [
|
|
1842
|
+
{
|
|
1843
|
+
uri,
|
|
1844
|
+
mimeType: "application/json",
|
|
1845
|
+
text: JSON.stringify(adUnits, null, 2)
|
|
1846
|
+
}
|
|
1847
|
+
]
|
|
1848
|
+
};
|
|
1849
|
+
}
|
|
1850
|
+
throw new Error(`Unknown resource: ${uri}`);
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
// src/server/index.ts
|
|
1854
|
+
function createServer() {
|
|
1855
|
+
const server = new Server(
|
|
1856
|
+
{
|
|
1857
|
+
name: "adsense-mcp-server",
|
|
1858
|
+
version: "0.1.0"
|
|
1859
|
+
},
|
|
1860
|
+
{
|
|
1861
|
+
capabilities: {
|
|
1862
|
+
tools: {},
|
|
1863
|
+
resources: {}
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
);
|
|
1867
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
1868
|
+
return {
|
|
1869
|
+
tools: registerTools()
|
|
1870
|
+
};
|
|
1871
|
+
});
|
|
1872
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1873
|
+
return handleToolCall(request.params.name, request.params.arguments || {});
|
|
1874
|
+
});
|
|
1875
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
1876
|
+
return handleListResources();
|
|
1877
|
+
});
|
|
1878
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
1879
|
+
return handleReadResource(request.params.uri);
|
|
1880
|
+
});
|
|
1881
|
+
return server;
|
|
1882
|
+
}
|
|
1883
|
+
async function startServer(accountId) {
|
|
1884
|
+
if (accountId) {
|
|
1885
|
+
process.env.ADSENSE_ACCOUNT_ID = accountId;
|
|
1886
|
+
}
|
|
1887
|
+
const server = createServer();
|
|
1888
|
+
const transport = new StdioServerTransport();
|
|
1889
|
+
await server.connect(transport);
|
|
1890
|
+
process.on("SIGINT", async () => {
|
|
1891
|
+
await server.close();
|
|
1892
|
+
process.exit(0);
|
|
1893
|
+
});
|
|
1894
|
+
process.on("SIGTERM", async () => {
|
|
1895
|
+
await server.close();
|
|
1896
|
+
process.exit(0);
|
|
1897
|
+
});
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
// src/cli/run.ts
|
|
1901
|
+
async function runServer(options) {
|
|
1902
|
+
if (options.account) {
|
|
1903
|
+
process.stderr.write(`Using account: ${options.account}
|
|
1904
|
+
`);
|
|
1905
|
+
}
|
|
1906
|
+
await startServer(options.account);
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
// src/cli/index.ts
|
|
1910
|
+
var program = new Command();
|
|
1911
|
+
program.name("adsense-mcp").description("AdSense MCP Server - Query earnings, reports, alerts via Claude").version("0.1.0");
|
|
1912
|
+
program.command("init").description("Initialize OAuth authentication with Google AdSense").option("--client-id <id>", "OAuth Client ID").option("--client-secret <secret>", "OAuth Client Secret").option("--service-account <path>", "Path to service account JSON key file").action(async (options) => {
|
|
1913
|
+
await runInit(options);
|
|
1914
|
+
});
|
|
1915
|
+
program.command("doctor").description("Check authentication and API access").action(async () => {
|
|
1916
|
+
await runDoctor();
|
|
1917
|
+
});
|
|
1918
|
+
program.command("run").description("Start the MCP server").option("--account <id>", "Default AdSense account ID to use").action(async (options) => {
|
|
1919
|
+
await runServer(options);
|
|
1920
|
+
});
|
|
1921
|
+
program.argument("[command]", "Command to run").action(async (cmd) => {
|
|
1922
|
+
if (!cmd) {
|
|
1923
|
+
await runServer({});
|
|
1924
|
+
}
|
|
1925
|
+
});
|
|
1926
|
+
program.parse();
|
|
1927
|
+
//# sourceMappingURL=index.js.map
|