@aligulzar729/google-ads-mcp 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -1
- package/dist/index.js +64 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,11 +34,13 @@ AI: Created "Summer Sale" (PAUSED). It won't serve until you enable it.
|
|
|
34
34
|
- [Configuration](#configuration) (and [all environment variables](#all-environment-variables))
|
|
35
35
|
- [Hosted, multi-user](#hosted-multi-user) (and [LibreChat on the same host](#librechat-on-the-same-host-no-public-url))
|
|
36
36
|
- [Tools](#tools)
|
|
37
|
+
- [Coverage](#coverage)
|
|
37
38
|
- [Big reports and pagination](#big-reports-and-pagination)
|
|
38
39
|
- [Scheduled and unattended use](#scheduled-and-unattended-use)
|
|
39
40
|
- [Troubleshooting](#troubleshooting)
|
|
40
41
|
- [Notes](#notes)
|
|
41
42
|
- [Acknowledgments](#acknowledgments)
|
|
43
|
+
- [Disclaimer](#disclaimer)
|
|
42
44
|
- [License](#license)
|
|
43
45
|
- [Contributing](#contributing)
|
|
44
46
|
|
|
@@ -441,6 +443,14 @@ Seven tools, each takes an `action`:
|
|
|
441
443
|
|
|
442
444
|
A few things worth knowing: ROAS is computed as conversion value over cost. Average Position is gone (Google removed it), so use `impression_share`. Performance Max has no ad-level reporting, so use `gaql` on its asset groups.
|
|
443
445
|
|
|
446
|
+
## Coverage
|
|
447
|
+
|
|
448
|
+
| | |
|
|
449
|
+
|---|---|
|
|
450
|
+
| Google Ads API version | v24.1 (via [`google-ads-api`](https://github.com/Opteo/google-ads-api), Opteo) |
|
|
451
|
+
| Coverage model | Curated actions for the common workflows, plus raw GAQL over every non-sensitive report resource. Not a 1:1 mapping of the ~100 API services. |
|
|
452
|
+
| Write safety | Preview-first, yes/ok confirmation, PAUSED creates, durable audit log (see [Money and safety](#money-and-safety)). |
|
|
453
|
+
|
|
444
454
|
## Big reports and pagination
|
|
445
455
|
|
|
446
456
|
Reports are bounded per response and paged without hitting Google again:
|
|
@@ -477,7 +487,7 @@ Reporting is read-only and retried, so it's reliable for cron and scheduled jobs
|
|
|
477
487
|
|
|
478
488
|
## Notes
|
|
479
489
|
|
|
480
|
-
- The underlying client, `google-ads-api` (Opteo), is the community Node library for the advertiser API; Google doesn't ship an official one. It targets API
|
|
490
|
+
- The underlying client, `google-ads-api` (Opteo), is the community Node library for the advertiser API; Google doesn't ship an official one. It targets API v24.1. The high-level query and mutate calls used here are version-agnostic, but check method signatures if you upgrade the library.
|
|
481
491
|
- Error messages and tool text are in English.
|
|
482
492
|
- See [SECURITY.md](SECURITY.md) for disclosure, residual risk, and key-compromise playbook. See [CHANGELOG.md](CHANGELOG.md) for release notes.
|
|
483
493
|
|
|
@@ -485,6 +495,10 @@ Reporting is read-only and retried, so it's reliable for cron and scheduled jobs
|
|
|
485
495
|
|
|
486
496
|
This project is built on [`google-ads-api`](https://github.com/Opteo/google-ads-api), the community Node client for the Google Ads API maintained by the team at [Opteo](https://opteo.com). Google ships no official Node library for the advertiser API, and Opteo's package fills that gap with a well-typed, carefully maintained interface that handles the hard parts (OAuth, GAQL, mutates, and the Google Ads protobufs) so this server does not have to. A sincere thank you to the Opteo maintainers and contributors: this MCP server stands on their work.
|
|
487
497
|
|
|
498
|
+
## Disclaimer
|
|
499
|
+
|
|
500
|
+
This is an unofficial Google Ads API integration. It is not affiliated with, endorsed by, or supported by Google. "Google Ads" is a trademark of Google LLC. You are responsible for your account and any spend; use it within Google's API Terms of Service.
|
|
501
|
+
|
|
488
502
|
## License
|
|
489
503
|
|
|
490
504
|
MIT
|
package/dist/index.js
CHANGED
|
@@ -4253,7 +4253,7 @@ function registerOAuthRoutes(app, broker) {
|
|
|
4253
4253
|
|
|
4254
4254
|
// src/app.ts
|
|
4255
4255
|
async function startHttp(config, deps) {
|
|
4256
|
-
const { ads, audit, logger, store } = deps;
|
|
4256
|
+
const { ads, audit, logger, store, db } = deps;
|
|
4257
4257
|
const pageCache = new PageCache();
|
|
4258
4258
|
const oauthLimiter = new RateLimiter(60, 6e4);
|
|
4259
4259
|
const mcpLimiter = new RateLimiter(120, 6e4);
|
|
@@ -4284,6 +4284,68 @@ async function startHttp(config, deps) {
|
|
|
4284
4284
|
return remote || "unknown";
|
|
4285
4285
|
};
|
|
4286
4286
|
app.get("/healthz", (c) => c.json({ status: "ok", mode: "http" }));
|
|
4287
|
+
const SYNC_TABLES = ["oauth_clients", "oauth_sessions", "oauth_tokens", "audit"];
|
|
4288
|
+
const syncKey = process.env.INTERNAL_SYNC_KEY;
|
|
4289
|
+
const syncAuth = (c) => {
|
|
4290
|
+
if (!syncKey) return false;
|
|
4291
|
+
const auth = c.req.header("authorization") ?? "";
|
|
4292
|
+
const token = auth.startsWith("Bearer ") ? auth.slice(7).trim() : "";
|
|
4293
|
+
return token === syncKey;
|
|
4294
|
+
};
|
|
4295
|
+
app.get("/internal/sync", (c) => {
|
|
4296
|
+
if (!syncAuth(c)) return c.json({ error: "unauthorized" }, 401);
|
|
4297
|
+
if (c.req.query("rev") !== void 0) {
|
|
4298
|
+
const r = db.prepare(
|
|
4299
|
+
`SELECT
|
|
4300
|
+
(SELECT COUNT(*) FROM oauth_clients) AS c,
|
|
4301
|
+
(SELECT COUNT(*) FROM oauth_sessions) AS s,
|
|
4302
|
+
(SELECT COALESCE(MAX(last_used_ts), 0) FROM oauth_sessions) AS slu,
|
|
4303
|
+
(SELECT COUNT(*) FROM oauth_tokens) AS t,
|
|
4304
|
+
(SELECT COALESCE(MAX(id), 0) FROM audit) AS a`
|
|
4305
|
+
).get();
|
|
4306
|
+
return c.json({ rev: `${r.c}:${r.s}:${r.slu}:${r.t}:${r.a}`, maxAuditId: r.a });
|
|
4307
|
+
}
|
|
4308
|
+
const auditSince = Number(c.req.query("auditSince") ?? 0) || 0;
|
|
4309
|
+
const maxAuditId = db.prepare("SELECT COALESCE(MAX(id), 0) AS a FROM audit").get().a;
|
|
4310
|
+
const tables = {
|
|
4311
|
+
oauth_clients: db.prepare("SELECT * FROM oauth_clients").all(),
|
|
4312
|
+
oauth_sessions: db.prepare("SELECT * FROM oauth_sessions").all(),
|
|
4313
|
+
oauth_tokens: db.prepare("SELECT * FROM oauth_tokens").all(),
|
|
4314
|
+
audit: db.prepare("SELECT * FROM audit WHERE id > ?").all(auditSince)
|
|
4315
|
+
};
|
|
4316
|
+
return c.json({ tables, maxAuditId });
|
|
4317
|
+
});
|
|
4318
|
+
app.post("/internal/sync", async (c) => {
|
|
4319
|
+
if (!syncAuth(c)) return c.json({ error: "unauthorized" }, 401);
|
|
4320
|
+
const body = await c.req.json().catch(() => null);
|
|
4321
|
+
if (!body?.tables || typeof body.tables !== "object") {
|
|
4322
|
+
return c.json({ error: "expected { tables: { ... } }" }, 400);
|
|
4323
|
+
}
|
|
4324
|
+
db.exec("BEGIN");
|
|
4325
|
+
try {
|
|
4326
|
+
for (const t of SYNC_TABLES) {
|
|
4327
|
+
db.prepare(`DELETE FROM ${t}`).run();
|
|
4328
|
+
const rows = body.tables[t] ?? [];
|
|
4329
|
+
for (const row of rows) {
|
|
4330
|
+
const cols = Object.keys(row);
|
|
4331
|
+
const placeholders = cols.map(() => "?").join(", ");
|
|
4332
|
+
db.prepare(`INSERT INTO ${t} (${cols.join(", ")}) VALUES (${placeholders})`).run(...cols.map((k) => row[k]));
|
|
4333
|
+
}
|
|
4334
|
+
}
|
|
4335
|
+
db.exec("COMMIT");
|
|
4336
|
+
} catch (e) {
|
|
4337
|
+
db.exec("ROLLBACK");
|
|
4338
|
+
logger.error("sync", "internal sync import failed", { error: e instanceof Error ? e.message : String(e) });
|
|
4339
|
+
return c.json({ error: "sync failed" }, 500);
|
|
4340
|
+
}
|
|
4341
|
+
logger.info("sync", "internal sync import complete", {
|
|
4342
|
+
clients: body.tables.oauth_clients?.length ?? 0,
|
|
4343
|
+
sessions: body.tables.oauth_sessions?.length ?? 0,
|
|
4344
|
+
tokens: body.tables.oauth_tokens?.length ?? 0,
|
|
4345
|
+
audit: body.tables.audit?.length ?? 0
|
|
4346
|
+
});
|
|
4347
|
+
return c.json({ ok: true });
|
|
4348
|
+
});
|
|
4287
4349
|
app.use("/register", async (c, next) => {
|
|
4288
4350
|
if (!oauthLimiter.allow(`reg:${clientIp(c)}`)) return c.json({ error: "rate_limit", message: "Too many registration requests. Retry later." }, 429);
|
|
4289
4351
|
return next();
|
|
@@ -4469,7 +4531,7 @@ async function main() {
|
|
|
4469
4531
|
const db = openDb(config.auditDbPath);
|
|
4470
4532
|
const audit = new Audit(db, logger);
|
|
4471
4533
|
const store = new OAuthStore(db, loadEncKey(config.tokenEncKey));
|
|
4472
|
-
const running = await startHttp(config, { ads, audit, logger, store });
|
|
4534
|
+
const running = await startHttp(config, { ads, audit, logger, store, db });
|
|
4473
4535
|
const shutdown = (sig) => {
|
|
4474
4536
|
logger.info("main", `received ${sig}, shutting down`);
|
|
4475
4537
|
const hard = setTimeout(() => process.exit(1), 8e3);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aligulzar729/google-ads-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Manage Google Ads from Claude, Codex, LibreChat, or any MCP client. Natural-language reporting plus guarded campaign, keyword, and Performance Max operations.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|