@aligulzar729/google-ads-mcp 0.2.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 +169 -52
- package/dist/index.js +68 -13
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Ask for a report, launch a campaign, tune bids and budgets, manage keywords, or
|
|
|
10
10
|
|
|
11
11
|
The important part: reads are free and can never spend, and every write **previews first**. The assistant shows you the exact values and asks you to type **yes** or **ok** in chat; only then does it apply with `confirmed:true` and `user_confirmation`. New campaigns start paused. You get an AI copilot for Google Ads without pretending the model is a free pass on your budget.
|
|
12
12
|
|
|
13
|
-
> **Money and residual risk:** once something is enabled or a budget goes up, Google can spend real money. Previews and yes/ok are guardrails, not a bank lock, MCP cannot force a human click, and a sloppy model could invent approval. Read the preview. If you would not hit Apply in the Google Ads UI, do not type yes here. See [
|
|
13
|
+
> **Money and residual risk:** once something is enabled or a budget goes up, Google can spend real money. Previews and yes/ok are guardrails, not a bank lock, MCP cannot force a human click, and a sloppy model could invent approval. Read the preview. If you would not hit Apply in the Google Ads UI, do not type yes here. See [Money and safety](#money-and-safety) and [SECURITY.md](SECURITY.md).
|
|
14
14
|
|
|
15
15
|
```
|
|
16
16
|
You: How did my campaigns do last month?
|
|
@@ -23,6 +23,27 @@ You: yes
|
|
|
23
23
|
AI: Created "Summer Sale" (PAUSED). It won't serve until you enable it.
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
## Contents
|
|
27
|
+
|
|
28
|
+
- [Quick start](#quick-start)
|
|
29
|
+
- [What it can do](#what-it-can-do)
|
|
30
|
+
- [What this is not](#what-this-is-not)
|
|
31
|
+
- [Money and safety](#money-and-safety)
|
|
32
|
+
- [Add it to your AI harness](#add-it-to-your-ai-harness)
|
|
33
|
+
- [Credentials](#credentials) (and [Getting a refresh token](#getting-a-refresh-token-oauth-playground))
|
|
34
|
+
- [Configuration](#configuration) (and [all environment variables](#all-environment-variables))
|
|
35
|
+
- [Hosted, multi-user](#hosted-multi-user) (and [LibreChat on the same host](#librechat-on-the-same-host-no-public-url))
|
|
36
|
+
- [Tools](#tools)
|
|
37
|
+
- [Coverage](#coverage)
|
|
38
|
+
- [Big reports and pagination](#big-reports-and-pagination)
|
|
39
|
+
- [Scheduled and unattended use](#scheduled-and-unattended-use)
|
|
40
|
+
- [Troubleshooting](#troubleshooting)
|
|
41
|
+
- [Notes](#notes)
|
|
42
|
+
- [Acknowledgments](#acknowledgments)
|
|
43
|
+
- [Disclaimer](#disclaimer)
|
|
44
|
+
- [License](#license)
|
|
45
|
+
- [Contributing](#contributing)
|
|
46
|
+
|
|
26
47
|
## Quick start
|
|
27
48
|
|
|
28
49
|
```bash
|
|
@@ -40,16 +61,27 @@ cp .env.example .env # fill in values
|
|
|
40
61
|
npm run dev:stdio # or: npm run dev:http
|
|
41
62
|
```
|
|
42
63
|
|
|
43
|
-
Node **22.5+** is required
|
|
64
|
+
Node **22.5+** is required. There is no native build step and no database to run: HTTP mode uses Node's built-in SQLite, so `npx` just works.
|
|
44
65
|
|
|
45
66
|
## What it can do
|
|
46
67
|
|
|
47
68
|
- **Reporting**: campaign performance, Search impression share, Performance Max asset groups, and raw GAQL for anything else. It looks up real field names first, so queries don't fail on guessed fields.
|
|
48
|
-
- **Campaigns**: create Search, Display, Demand Gen,
|
|
69
|
+
- **Campaigns**: create Search, Display, Demand Gen, App, Shopping, and Performance Max campaigns (use Demand Gen for YouTube; Google no longer lets the API create Video campaigns); rename, set dates and networks, pause and resume, set budgets and bidding strategies, and target by location, language, and schedule.
|
|
49
70
|
- **Ad groups and ads**: manage ad groups and build ads per channel (responsive search, responsive display, HTML5, Demand Gen, video, app, shopping).
|
|
50
71
|
- **Keywords**: list with quality score, add positive or negative keywords, change bids and status.
|
|
51
72
|
- **Performance Max**: create asset groups from text and images, update or delete them, add more images, list and remove assets.
|
|
52
|
-
- **Accounts**: check the connection
|
|
73
|
+
- **Accounts**: check the connection and list the accounts you can reach; on the hosted (HTTP) server, also switch the active account and read an audit trail of every change.
|
|
74
|
+
|
|
75
|
+
### What it can't do
|
|
76
|
+
|
|
77
|
+
- **Conversion tracking.** Creating or editing conversion actions and importing offline conversions (it reads conversion metrics, but doesn't set them up).
|
|
78
|
+
- **Audiences and bid adjustments.** Remarketing or customer-match lists, audience targeting, and device, demographic, or audience bid modifiers. Targeting here is location, language, and ad schedule only.
|
|
79
|
+
- **Search ad assets / extensions.** Sitelinks, callouts, structured snippets, call, price, and promotion assets (Performance Max text and image assets are supported).
|
|
80
|
+
- **Account structure and budgets.** Creating customer accounts, managing an MCC hierarchy, or setting up billing and account-level budget orders.
|
|
81
|
+
- **Experiments, drafts, and recommendations.** A/B experiments, campaign drafts, and applying Google's optimization-score recommendations.
|
|
82
|
+
- **Shared library and labels.** Shared budgets, negative-keyword and placement-exclusion sets, and labels.
|
|
83
|
+
- **Keyword Planner.** Keyword ideas and traffic forecasts.
|
|
84
|
+
- **External links.** Merchant Center, Analytics, and YouTube account links (Shopping and Performance Max assume these already exist).
|
|
53
85
|
|
|
54
86
|
## What this is not
|
|
55
87
|
|
|
@@ -61,9 +93,9 @@ Node **22.5+** is required (HTTP mode uses Node's built-in SQLite).
|
|
|
61
93
|
|
|
62
94
|
If you want a report in chat, a careful campaign draft, or help wiring keywords and P-Max assets with a human still holding the wheel, you are in the right place. If you want unsupervised budget AI, you are not.
|
|
63
95
|
|
|
64
|
-
##
|
|
96
|
+
## Money and safety
|
|
65
97
|
|
|
66
|
-
Reads never cost anything. Writes can.
|
|
98
|
+
Reads never cost anything. Writes can, and every write **previews first**: the model shows the exact values and asks you to type **yes** or **ok**; apply then requires `confirmed:true` and `user_confirmation` `"yes"` or `"ok"` (`REQUIRE_USER_CONFIRMATION`, default on).
|
|
67
99
|
|
|
68
100
|
| Action | Money impact |
|
|
69
101
|
|--------|----------------|
|
|
@@ -72,33 +104,18 @@ Reads never cost anything. Writes can.
|
|
|
72
104
|
| Raise daily budget, change bids, enable a campaign or ad group | Can spend as soon as Google serves. |
|
|
73
105
|
| Pause or remove | Stops or deletes serving; removes are permanent for that entity. |
|
|
74
106
|
|
|
75
|
-
|
|
107
|
+
More guardrails: new campaigns and P-Max asset groups start **paused**; deletes show a clear warning first (also gated by `REQUIRE_USER_CONFIRMATION`); every applied change lands in a durable audit log; and HTTP mode rate-limits OAuth and `/mcp` per client IP (process-local, put a reverse proxy in front for multi-replica).
|
|
108
|
+
|
|
109
|
+
Habits that save pain:
|
|
76
110
|
|
|
77
111
|
- Start on a **test or low-budget** account while you learn the tools.
|
|
78
112
|
- Treat **yes** like approving a wire: skim the preview numbers (budget, bid, status) before you type it.
|
|
79
113
|
- Prefer **PAUSED** creates, then enable only what you meant to turn on.
|
|
80
|
-
- Shared budgets are shared
|
|
81
|
-
- Disabling chat confirm (`REQUIRE_USER_CONFIRMATION=false`) is for trusted automation only. On a live account with real spend, leave it on.
|
|
82
|
-
|
|
83
|
-
You are responsible for the Google Ads account and any charges that come from it. This project is MIT software that talks to Google's API; it does not pay your media bill and it does not absorb mistakes.
|
|
114
|
+
- Shared budgets are shared: changing one campaign's budget can hit every campaign on that budget (the tools warn when they can).
|
|
84
115
|
|
|
85
|
-
|
|
116
|
+
`REQUIRE_USER_CONFIRMATION=false` disables the chat yes/ok so one-shot `confirmed:true` works, for trusted headless automation only. On a live account with real spend, leave it on.
|
|
86
117
|
|
|
87
|
-
|
|
88
|
-
- Every write **previews first**. The model must show you the values and ask you to type **yes** or **ok**. Apply requires `confirmed:true` and `user_confirmation` `"yes"` or `"ok"` (`REQUIRE_USER_CONFIRMATION`, default on).
|
|
89
|
-
- New campaigns and Performance Max asset groups are created **paused**, so they never serve or spend until you turn them on.
|
|
90
|
-
- Deletes show a clear warning before they run (gated by `REQUIRE_USER_CONFIRMATION`, default on).
|
|
91
|
-
- Every applied change is recorded in a durable audit log.
|
|
92
|
-
- HTTP mode rate-limits OAuth and `/mcp` per client IP (process-local; put a reverse proxy in front for multi-replica).
|
|
93
|
-
|
|
94
|
-
Trusted headless automation only: set `REQUIRE_USER_CONFIRMATION=false` so one-shot `confirmed:true` works without a chat yes/ok. Do that only when you already trust the caller with the account.
|
|
95
|
-
|
|
96
|
-
## Requirements
|
|
97
|
-
|
|
98
|
-
- Node 22.5 or newer.
|
|
99
|
-
- Google Ads API credentials (see [Credentials](#credentials)).
|
|
100
|
-
|
|
101
|
-
No native build step and no database to run: it uses Node's built-in SQLite, so `npx` just works.
|
|
118
|
+
You are responsible for the Google Ads account and any charges from it. This is MIT software that talks to Google's API; it does not pay your media bill and does not absorb mistakes.
|
|
102
119
|
|
|
103
120
|
## Add it to your AI harness
|
|
104
121
|
|
|
@@ -199,7 +216,7 @@ env = { GOOGLE_ADS_DEVELOPER_TOKEN = "...", GOOGLE_OAUTH_CLIENT_ID = "...", GOOG
|
|
|
199
216
|
|
|
200
217
|
<br>
|
|
201
218
|
|
|
202
|
-
LibreChat connects over HTTP and lets each user sign in with their own Google account, so you run the server yourself (see [Hosted, multi-user](#hosted-multi-user)) and point LibreChat at it
|
|
219
|
+
LibreChat connects over HTTP and lets each user sign in with their own Google account, so you run the server yourself (see [Hosted, multi-user](#hosted-multi-user)) and point LibreChat at it:
|
|
203
220
|
|
|
204
221
|
```yaml
|
|
205
222
|
mcpServers:
|
|
@@ -207,9 +224,15 @@ mcpServers:
|
|
|
207
224
|
type: streamable-http
|
|
208
225
|
url: "https://your-subdomain.example.com/mcp"
|
|
209
226
|
startup: false
|
|
227
|
+
requiresOAuth: true
|
|
210
228
|
```
|
|
211
229
|
|
|
212
|
-
|
|
230
|
+
Two settings matter:
|
|
231
|
+
|
|
232
|
+
- `requiresOAuth: true` declares this a per-user OAuth server. Without it, LibreChat tries to auto-detect OAuth by opening an unauthenticated connection to list tools, which always fails on an auth-walled server and fills your logs with `OAuth not supported in unauthenticated discovery` and repeated `Reinspection failed ... timeout`. The flag skips that probe and shows the sign-in prompt directly.
|
|
233
|
+
- `startup: false` connects on first use, not at boot, since there is no user to authorize at startup.
|
|
234
|
+
|
|
235
|
+
Also add the host to `mcpSettings.allowedDomains` in `librechat.yaml`. Users then click Connect in chat and approve at Google. Nothing to paste. To run LibreChat and this server on one machine with no public URL, see [LibreChat on the same host](#librechat-on-the-same-host-no-public-url).
|
|
213
236
|
</details>
|
|
214
237
|
|
|
215
238
|
<details>
|
|
@@ -259,17 +282,33 @@ Three values are always required, because they identify the app (not the user):
|
|
|
259
282
|
|
|
260
283
|
For local (stdio) use, add one account's own credentials:
|
|
261
284
|
|
|
262
|
-
- `GOOGLE_ADS_REFRESH_TOKEN`: a refresh token for a Google account with access to the ads account.
|
|
263
|
-
- `GOOGLE_ADS_CUSTOMER_ID`: the
|
|
264
|
-
- `GOOGLE_ADS_LOGIN_CUSTOMER_ID`: optional
|
|
285
|
+
- `GOOGLE_ADS_REFRESH_TOKEN`: a refresh token for a Google account with access to the ads account. See [Getting a refresh token](#getting-a-refresh-token-oauth-playground) below.
|
|
286
|
+
- `GOOGLE_ADS_CUSTOMER_ID`: the account you act on, 10 digits with no dashes. Its campaigns and reports are what you read and change.
|
|
287
|
+
- `GOOGLE_ADS_LOGIN_CUSTOMER_ID`: optional. The manager (MCC) account you reach that account *through*, if any.
|
|
288
|
+
|
|
289
|
+
These two trip everyone up, so plainly: `CUSTOMER_ID` is **which account to act on**, `LOGIN_CUSTOMER_ID` is **which manager to go through to reach it**. A manager (MCC) account holds no campaigns itself; it just grants access to the client accounts under it. So:
|
|
290
|
+
|
|
291
|
+
- Your Google user has **direct** access to the account (it shows as `[client]` in `google_ads_account list`): set `CUSTOMER_ID` to it and **leave `LOGIN_CUSTOMER_ID` empty**.
|
|
292
|
+
- You only reach the account **through a manager**: set `CUSTOMER_ID` to the client account and `LOGIN_CUSTOMER_ID` to that **manager's** id, never to the client id, and never to a manager that does not actually own the client (both cause `PERMISSION_DENIED`).
|
|
265
293
|
|
|
266
294
|
Keep the OAuth app in the **In Production** publishing status. In Testing, refresh tokens expire after 7 days.
|
|
267
295
|
|
|
268
296
|
For hosted (http) use you don't set a refresh token at all. Users authorize themselves in the browser and the server stores each token encrypted. See below.
|
|
269
297
|
|
|
298
|
+
### Getting a refresh token (OAuth Playground)
|
|
299
|
+
|
|
300
|
+
For stdio mode you supply one account's refresh token. Two settings are what everyone gets wrong, so both steps below are required:
|
|
301
|
+
|
|
302
|
+
1. **Add the Playground redirect to your OAuth client.** In Google Cloud Console, open your OAuth client (the one whose id and secret you use) under APIs & Services, then Credentials. Under **Authorized redirect URIs**, add `https://developers.google.com/oauthplayground` and save. Miss this and the Playground fails with `redirect_uri_mismatch`.
|
|
303
|
+
2. **Use your own client in the Playground.** Open the [OAuth 2.0 Playground](https://developers.google.com/oauthplayground), click the **gear** (top right), check **Use your own OAuth credentials**, and paste the same client id and secret. Miss this and the token is minted for Google's demo client, which later fails with `unauthorized_client`.
|
|
304
|
+
3. **Authorize the Ads scope.** In the left panel under "Input your own scopes", enter `https://www.googleapis.com/auth/adwords`, click **Authorize APIs**, and sign in with the Google account that can reach the ads account.
|
|
305
|
+
4. **Exchange for the token.** Click **Exchange authorization code for tokens** and copy the `refresh_token` into `GOOGLE_ADS_REFRESH_TOKEN`.
|
|
306
|
+
|
|
307
|
+
A token works only when both are true: it was minted by *your* client (step 2) and *your* client permits the Playground redirect (step 1). The Playground redirect is only needed for minting, so you can remove it from the client afterward; it is separate from the `PUBLIC_BASE_URL/auth/callback` redirect that http mode registers.
|
|
308
|
+
|
|
270
309
|
## Configuration
|
|
271
310
|
|
|
272
|
-
Everything is set through environment variables
|
|
311
|
+
Everything is set through environment variables. Every setting can also be passed as a command-line flag: the flag is the env var name, lowercased, with dashes for underscores. A bare `--flag` sets `true`, and `--no-flag` sets `false`. Flags are handy in a client's `args` or a launch command:
|
|
273
312
|
|
|
274
313
|
```bash
|
|
275
314
|
npx -y @aligulzar729/google-ads-mcp \
|
|
@@ -278,20 +317,47 @@ npx -y @aligulzar729/google-ads-mcp \
|
|
|
278
317
|
--no-require-user-confirmation # trusted automation only
|
|
279
318
|
```
|
|
280
319
|
|
|
281
|
-
In a client config, add them to `args`:
|
|
282
|
-
|
|
283
320
|
```json
|
|
284
321
|
"args": ["-y", "@aligulzar729/google-ads-mcp", "--app-name=Acme Ads"]
|
|
285
322
|
```
|
|
286
323
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
| HTTP port | `PORT` / `--port` | default 3001 |
|
|
324
|
+
Prefer env vars for secrets: flags can show up in the process list.
|
|
325
|
+
|
|
326
|
+
### All environment variables
|
|
327
|
+
|
|
328
|
+
**App identity, required in both modes.** These identify the app, not the user.
|
|
293
329
|
|
|
294
|
-
|
|
330
|
+
| Env var | Flag | Description |
|
|
331
|
+
|---------|------|-------------|
|
|
332
|
+
| `GOOGLE_ADS_DEVELOPER_TOKEN` | `--google-ads-developer-token` | Google Ads API developer token, from your manager account API Center. |
|
|
333
|
+
| `GOOGLE_OAUTH_CLIENT_ID` | `--google-oauth-client-id` | OAuth client id from a Google Cloud project with the Google Ads API enabled. |
|
|
334
|
+
| `GOOGLE_OAUTH_CLIENT_SECRET` | `--google-oauth-client-secret` | OAuth client secret. |
|
|
335
|
+
|
|
336
|
+
**Behavior, both modes.**
|
|
337
|
+
|
|
338
|
+
| Env var | Flag | Default | Description |
|
|
339
|
+
|---------|------|---------|-------------|
|
|
340
|
+
| `APP_NAME` | `--app-name` | `Google Ads MCP` | Name shown on the OAuth consent and error pages. |
|
|
341
|
+
| `LOG_LEVEL` | `--log-level` | `info` | `debug`, `info`, `warn`, or `error`. |
|
|
342
|
+
| `REQUIRE_USER_CONFIRMATION` | `--no-require-user-confirmation` | on | Gates every write, including deletes. Turn off only for trusted headless automation. |
|
|
343
|
+
|
|
344
|
+
**stdio mode, single user.** One account's own credentials.
|
|
345
|
+
|
|
346
|
+
| Env var | Flag | Default | Description |
|
|
347
|
+
|---------|------|---------|-------------|
|
|
348
|
+
| `GOOGLE_ADS_REFRESH_TOKEN` | `--google-ads-refresh-token` | empty | Refresh token for a Google account with access to the ads account. See [Getting a refresh token](#getting-a-refresh-token-oauth-playground). |
|
|
349
|
+
| `GOOGLE_ADS_CUSTOMER_ID` | `--google-ads-customer-id` | empty | The account to act on (10 digits, no dashes). |
|
|
350
|
+
| `GOOGLE_ADS_LOGIN_CUSTOMER_ID` | `--google-ads-login-customer-id` | empty | The manager (MCC) to reach it through; leave empty for direct access. See [Credentials](#credentials). |
|
|
351
|
+
|
|
352
|
+
**http mode, hosted and multi-user.** Per-user credentials come from each user's browser sign-in, so you do not set a refresh token here.
|
|
353
|
+
|
|
354
|
+
| Env var | Flag | Default | Description |
|
|
355
|
+
|---------|------|---------|-------------|
|
|
356
|
+
| `PORT` | `--port` | `3001` | Listen port. |
|
|
357
|
+
| `PUBLIC_BASE_URL` | `--public-base-url` | required | Public base URL of this instance. Register `PUBLIC_BASE_URL/auth/callback` as a redirect URI on the Google OAuth client. |
|
|
358
|
+
| `TOKEN_ENC_KEY` | `--token-enc-key` | required | 32-byte base64 key (`openssl rand -base64 32`) that encrypts stored refresh tokens. Keep it stable and secret. |
|
|
359
|
+
| `ALLOWED_REDIRECT_ORIGINS` | `--allowed-redirect-origins` | required | Comma-separated allowlist of client origins allowed to complete the OAuth flow, for example `https://chat.example.com`. |
|
|
360
|
+
| `AUDIT_DB_PATH` | `--audit-db-path` | `./data/audit.db` | Where the audit log and encrypted authorizations live. Mount a volume so it survives restarts. |
|
|
295
361
|
|
|
296
362
|
## Hosted, multi-user
|
|
297
363
|
|
|
@@ -303,13 +369,7 @@ Run it (behind HTTPS, on its own subdomain):
|
|
|
303
369
|
npx -y @aligulzar729/google-ads-mcp --http
|
|
304
370
|
```
|
|
305
371
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
- `TOKEN_ENC_KEY`: 32-byte base64 key that encrypts stored refresh tokens. Generate with `openssl rand -base64 32`. Keep it stable and secret; rotating it invalidates every stored authorization.
|
|
309
|
-
- `PUBLIC_BASE_URL`: the public HTTPS base URL of this instance, for example `https://ads.example.com`. Register `${PUBLIC_BASE_URL}/auth/callback` as a redirect URI on your Google OAuth client.
|
|
310
|
-
- `ALLOWED_REDIRECT_ORIGINS`: comma-separated allowlist of client origins allowed to complete the OAuth flow, for example `https://chat.example.com`. This is what stops a phished authorize link from delivering a code to an attacker.
|
|
311
|
-
- `PORT`: defaults to 3001.
|
|
312
|
-
- `AUDIT_DB_PATH`: where the audit log and encrypted authorizations live. Mount a volume so it survives restarts.
|
|
372
|
+
Beyond the shared app credentials, this mode needs `TOKEN_ENC_KEY`, `PUBLIC_BASE_URL`, and `ALLOWED_REDIRECT_ORIGINS`, plus optional `PORT` and `AUDIT_DB_PATH` (see [All environment variables](#all-environment-variables)). Two things worth calling out: register `PUBLIC_BASE_URL/auth/callback` as a redirect URI on your Google OAuth client, and `ALLOWED_REDIRECT_ORIGINS` is the allowlist that stops a phished authorize link from delivering a code to an attacker.
|
|
313
373
|
|
|
314
374
|
A `Dockerfile` and `docker-compose.yml` are included for a container deployment.
|
|
315
375
|
|
|
@@ -326,6 +386,47 @@ curl -sS http://127.0.0.1:3001/healthz
|
|
|
326
386
|
|
|
327
387
|
Point your reverse proxy (TLS) at `127.0.0.1:3001`. The **whole subdomain** must reach this container: OAuth discovery, `/authorize`, `/token`, and `/mcp` all live on the same host (`PUBLIC_BASE_URL`). Back up the `ads_data` volume (audit log + encrypted authorizations).
|
|
328
388
|
|
|
389
|
+
### LibreChat on the same host (no public URL)
|
|
390
|
+
|
|
391
|
+
To run LibreChat and this server on one machine over plain HTTP, with no public domain or TLS, run this server as a sidecar that shares the LibreChat api container's network namespace. That makes `http://localhost:3001` the same address for both your browser and LibreChat's backend, and Google allows `http://localhost` as the one non-HTTPS OAuth redirect, so sign-in works without a certificate.
|
|
392
|
+
|
|
393
|
+
Add to LibreChat's `docker-compose.override.yml`:
|
|
394
|
+
|
|
395
|
+
```yaml
|
|
396
|
+
services:
|
|
397
|
+
api:
|
|
398
|
+
ports:
|
|
399
|
+
- "3001:3001" # expose the sidecar to your browser
|
|
400
|
+
google-ads-mcp:
|
|
401
|
+
image: node:24-bookworm-slim
|
|
402
|
+
command: ["npx", "-y", "@aligulzar729/google-ads-mcp", "--http"]
|
|
403
|
+
network_mode: "service:api" # share the api netns: one localhost for browser and backend
|
|
404
|
+
depends_on:
|
|
405
|
+
- api
|
|
406
|
+
environment:
|
|
407
|
+
PORT: "3001"
|
|
408
|
+
PUBLIC_BASE_URL: "http://localhost:3001"
|
|
409
|
+
ALLOWED_REDIRECT_ORIGINS: "http://localhost:3080" # your LibreChat origin
|
|
410
|
+
GOOGLE_ADS_DEVELOPER_TOKEN: ${GOOGLE_ADS_DEVELOPER_TOKEN}
|
|
411
|
+
GOOGLE_OAUTH_CLIENT_ID: ${GOOGLE_OAUTH_CLIENT_ID}
|
|
412
|
+
GOOGLE_OAUTH_CLIENT_SECRET: ${GOOGLE_OAUTH_CLIENT_SECRET}
|
|
413
|
+
TOKEN_ENC_KEY: ${TOKEN_ENC_KEY}
|
|
414
|
+
AUDIT_DB_PATH: /app/data/audit.db
|
|
415
|
+
volumes:
|
|
416
|
+
- ads_data:/app/data
|
|
417
|
+
volumes:
|
|
418
|
+
ads_data:
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
Then:
|
|
422
|
+
|
|
423
|
+
- Point the LibreChat entry at `url: "http://localhost:3001/mcp"` with `requiresOAuth: true` (see [LibreChat](#add-it-to-your-ai-harness) above), and keep `localhost` in `mcpSettings.allowedDomains`.
|
|
424
|
+
- Register `http://localhost:3001/auth/callback` as a redirect URI on the Google OAuth client.
|
|
425
|
+
- Sign in from Chrome or Firefox: the consent cookie is `Secure`, and those treat `localhost` as a secure context. Safari may refuse it.
|
|
426
|
+
- The sidecar shares the api container's network, so recreate both together after changes: `docker compose up -d --force-recreate api google-ads-mcp`.
|
|
427
|
+
|
|
428
|
+
Publish the OAuth app to In Production so per-user tokens do not expire after 7 days.
|
|
429
|
+
|
|
329
430
|
## Tools
|
|
330
431
|
|
|
331
432
|
Seven tools, each takes an `action`:
|
|
@@ -342,6 +443,14 @@ Seven tools, each takes an `action`:
|
|
|
342
443
|
|
|
343
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.
|
|
344
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
|
+
|
|
345
454
|
## Big reports and pagination
|
|
346
455
|
|
|
347
456
|
Reports are bounded per response and paged without hitting Google again:
|
|
@@ -365,16 +474,20 @@ Reporting is read-only and retried, so it's reliable for cron and scheduled jobs
|
|
|
365
474
|
| Symptom | What to try |
|
|
366
475
|
|---------|-------------|
|
|
367
476
|
| Refresh token dies after ~7 days | OAuth app is still in **Testing**. Publish it to **In Production**. |
|
|
368
|
-
| `PERMISSION_DENIED` /
|
|
477
|
+
| `PERMISSION_DENIED` / "set login-customer-id" | `GOOGLE_ADS_LOGIN_CUSTOMER_ID` is only for reaching a client *through a manager*. For an account you access directly (it shows as `[client]` in `account list`), leave it empty. Setting a manager you do not reach that client through, or setting one at all when none is needed, causes this error. If it is still denied, the login genuinely lacks access to that customer id. |
|
|
478
|
+
| `unauthorized_client` on token exchange | The refresh token was minted by a different OAuth client than `GOOGLE_OAUTH_CLIENT_ID`. Re-mint it with that exact client (in the OAuth Playground, check "Use your own OAuth credentials" first). |
|
|
369
479
|
| Quota / rate errors | Developer token may need **Standard Access**. Reads are retried with backoff. |
|
|
370
480
|
| "user_confirmation" / confirmed rejected | Call once without `confirmed` to preview, ask the user to type yes or ok, then re-call with `confirmed:true` and `user_confirmation:"yes"` (or `"ok"`). |
|
|
371
481
|
| "not connected" / reconnect message | Refresh token revoked; re-auth (HTTP) or mint a new refresh token (stdio). |
|
|
372
482
|
| HTTP container exits on boot | Missing `TOKEN_ENC_KEY`, `PUBLIC_BASE_URL`, or `ALLOWED_REDIRECT_ORIGINS`. Check logs. |
|
|
373
|
-
| OAuth works but LibreChat never connects | `startup: false` on the MCP
|
|
483
|
+
| OAuth works but LibreChat never connects | Set `requiresOAuth: true` and `startup: false` on the MCP entry, add the host to `allowedDomains`, and point `url` at the public `/mcp` path. |
|
|
484
|
+
| LibreChat logs `OAuth not supported in unauthenticated discovery` or repeated `Reinspection failed ... timeout` | Missing `requiresOAuth: true`. LibreChat is probing the server without auth to list tools, which always fails on an OAuth server. The flag silences the probe and shows the sign-in prompt directly. |
|
|
485
|
+
| Stale connection after changing the server URL under the same name | LibreChat caches OAuth tokens per server name. Reconnect, or clear its cached tokens for that server, so it registers fresh against the new backend. |
|
|
486
|
+
| Google returns `redirect_uri_mismatch` mid sign-in | `PUBLIC_BASE_URL/auth/callback` is not registered on the OAuth client. For localhost that is `http://localhost:3001/auth/callback`. |
|
|
374
487
|
|
|
375
488
|
## Notes
|
|
376
489
|
|
|
377
|
-
- 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.
|
|
378
491
|
- Error messages and tool text are in English.
|
|
379
492
|
- See [SECURITY.md](SECURITY.md) for disclosure, residual risk, and key-compromise playbook. See [CHANGELOG.md](CHANGELOG.md) for release notes.
|
|
380
493
|
|
|
@@ -382,6 +495,10 @@ Reporting is read-only and retried, so it's reliable for cron and scheduled jobs
|
|
|
382
495
|
|
|
383
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.
|
|
384
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
|
+
|
|
385
502
|
## License
|
|
386
503
|
|
|
387
504
|
MIT
|
package/dist/index.js
CHANGED
|
@@ -1995,7 +1995,7 @@ var metadataHandler = {
|
|
|
1995
1995
|
// src/tools/orchestrators/report.ts
|
|
1996
1996
|
var reportTool = defineOrchestrator({
|
|
1997
1997
|
name: "google_ads_report",
|
|
1998
|
-
purpose:
|
|
1998
|
+
purpose: 'Read Google Ads performance. Prefer a curated action: campaign_performance for any channel, impression_share for Search visibility (the Average Position replacement), or asset_group for Performance Max. For anything else, daily or weekly trends, keyword or search-term views, custom field combinations, use gaql after calling metadata first. Resource hints for less obvious asks: "cart / basket value / cross-sell sales" -> cart_data_sales_view ; "iOS vs Android breakdown" -> add the segment segments.mobile_device_platform. Always call metadata on a resource before querying it to get its exact field names.',
|
|
1999
1999
|
limitations: "Read-only. ROAS is computed as conversion value divided by cost. Average Position does not exist; use action impression_share instead. impression_share only applies to Search campaigns; asset_group only applies to Performance Max. Curated reports already format values, but in raw gaql results, money fields are in micros (divide by 1,000,000) and rates (metrics.ctr, conversion rate, impression share) are ratios from 0 to 1. For gaql, do not guess field names: call metadata first to get the exact selectable, filterable, and sortable fields plus compatible metrics and segments, then use those names verbatim.",
|
|
2000
2000
|
examples: [
|
|
2001
2001
|
'{"action":"campaign_performance","date_range":"LAST_30_DAYS"}',
|
|
@@ -2063,7 +2063,7 @@ function langId(s) {
|
|
|
2063
2063
|
// src/tools/handlers/campaign/create.ts
|
|
2064
2064
|
var schema10 = z13.object({
|
|
2065
2065
|
name: z13.string().min(1).max(255),
|
|
2066
|
-
channel_type: z13.enum(["SEARCH", "PERFORMANCE_MAX", "DISPLAY", "DEMAND_GEN", "
|
|
2066
|
+
channel_type: z13.enum(["SEARCH", "PERFORMANCE_MAX", "DISPLAY", "DEMAND_GEN", "APP", "SHOPPING"]),
|
|
2067
2067
|
daily_budget: z13.number().positive().describe("Daily amount in account currency (e.g. 50 = 50.00/day)"),
|
|
2068
2068
|
bidding: z13.enum(BIDDING).default("MAXIMIZE_CONVERSIONS"),
|
|
2069
2069
|
target_cpa: z13.number().positive().optional().describe("Target CPA in account currency (TARGET_CPA / MAXIMIZE_CONVERSIONS)"),
|
|
@@ -2077,7 +2077,7 @@ var schema10 = z13.object({
|
|
|
2077
2077
|
});
|
|
2078
2078
|
var createHandler = {
|
|
2079
2079
|
action: "create",
|
|
2080
|
-
summary: "Create a campaign plus its daily budget, atomically. channel_type: SEARCH, DISPLAY, DEMAND_GEN
|
|
2080
|
+
summary: "Create a campaign plus its daily budget, atomically. channel_type: SEARCH, DISPLAY, DEMAND_GEN (use this for YouTube; Google no longer lets the API create Video campaigns), PERFORMANCE_MAX, APP (needs app_id + app_store, TARGET_CPA bidding), or SHOPPING (needs merchant_id from a linked Merchant Center). Created PAUSED. Preview first (omit confirmed). Then ask the user to type yes or ok in chat; only after that re-call with confirmed:true and user_confirmation yes|ok.",
|
|
2081
2081
|
readOnly: false,
|
|
2082
2082
|
category: "mutate",
|
|
2083
2083
|
schema: schema10,
|
|
@@ -2102,9 +2102,6 @@ Ask the user, then call create again with brand_guidelines_enabled set to true o
|
|
|
2102
2102
|
if (params.channel_type === "DEMAND_GEN" && params.bidding === "MANUAL_CPC") {
|
|
2103
2103
|
throw new Error("Demand Gen requires automated bidding (not MANUAL_CPC): use MAXIMIZE_CONVERSIONS, TARGET_CPA, MAXIMIZE_CONVERSION_VALUE, or TARGET_ROAS.");
|
|
2104
2104
|
}
|
|
2105
|
-
if (params.channel_type === "VIDEO" && params.bidding !== "MAXIMIZE_CONVERSIONS" && params.bidding !== "TARGET_CPA") {
|
|
2106
|
-
throw new Error("A Video Action campaign requires automated bidding (MAXIMIZE_CONVERSIONS or TARGET_CPA), not MANUAL_CPC.");
|
|
2107
|
-
}
|
|
2108
2105
|
if (params.channel_type === "APP") {
|
|
2109
2106
|
if (!params.app_id) throw new Error("app_id is required for an App campaign (the Android package name or numeric iOS App Store id).");
|
|
2110
2107
|
if (!params.app_store) throw new Error("app_store is required for an App campaign (GOOGLE_APP_STORE or APPLE_APP_STORE).");
|
|
@@ -2119,7 +2116,6 @@ Ask the user, then call create again with brand_guidelines_enabled set to true o
|
|
|
2119
2116
|
SEARCH: enums3.AdvertisingChannelType.SEARCH,
|
|
2120
2117
|
DISPLAY: enums3.AdvertisingChannelType.DISPLAY,
|
|
2121
2118
|
DEMAND_GEN: enums3.AdvertisingChannelType.DEMAND_GEN,
|
|
2122
|
-
VIDEO: enums3.AdvertisingChannelType.VIDEO,
|
|
2123
2119
|
PERFORMANCE_MAX: enums3.AdvertisingChannelType.PERFORMANCE_MAX,
|
|
2124
2120
|
SHOPPING: enums3.AdvertisingChannelType.SHOPPING,
|
|
2125
2121
|
APP: enums3.AdvertisingChannelType.MULTI_CHANNEL
|
|
@@ -2139,8 +2135,6 @@ Ask the user, then call create again with brand_guidelines_enabled set to true o
|
|
|
2139
2135
|
campaign.network_settings = { target_google_search: true, target_search_network: true };
|
|
2140
2136
|
} else if (params.channel_type === "DISPLAY") {
|
|
2141
2137
|
campaign.network_settings = { target_content_network: true };
|
|
2142
|
-
} else if (params.channel_type === "VIDEO") {
|
|
2143
|
-
campaign.advertising_channel_sub_type = enums3.AdvertisingChannelSubType.VIDEO_ACTION;
|
|
2144
2138
|
} else if (params.channel_type === "APP") {
|
|
2145
2139
|
campaign.advertising_channel_sub_type = enums3.AdvertisingChannelSubType.APP_CAMPAIGN;
|
|
2146
2140
|
campaign.app_campaign_setting = {
|
|
@@ -2183,7 +2177,6 @@ Ask the user, then call create again with brand_guidelines_enabled set to true o
|
|
|
2183
2177
|
PERFORMANCE_MAX: "\nNext: add an asset group before enabling, a P-MAX campaign needs assets to serve.",
|
|
2184
2178
|
DISPLAY: "\nNext: add an ad group, then a responsive display ad (add_ad ad_type RESPONSIVE_DISPLAY_AD) or an HTML5 ad, then enable the campaign.",
|
|
2185
2179
|
DEMAND_GEN: "\nNext: add an ad group, then a Demand Gen ad (add_ad ad_type DEMAND_GEN_MULTI_ASSET), then enable the campaign.",
|
|
2186
|
-
VIDEO: "\nNext: add an ad group, then a responsive video ad (add_ad ad_type VIDEO_RESPONSIVE with a YouTube video), then enable the campaign.",
|
|
2187
2180
|
APP: "\nNext: App campaigns auto-create an ad group, find it with ad_group list_ads (or a report), add an app ad (add_ad ad_type APP), then enable the campaign.",
|
|
2188
2181
|
SHOPPING: "\nNext: add an ad group, then a product ad (add_ad ad_type SHOPPING_PRODUCT). Products serve from the linked Merchant Center feed, make sure Merchant Center is linked.",
|
|
2189
2182
|
SEARCH: "\nNext: add an ad group and a responsive search ad, then enable the campaign."
|
|
@@ -2481,7 +2474,7 @@ var targetingHandler = {
|
|
|
2481
2474
|
var campaignTool = defineOrchestrator({
|
|
2482
2475
|
name: "google_ads_campaign",
|
|
2483
2476
|
purpose: "Create and manage campaigns. create builds a new one for Search, Display, Demand Gen, App, Shopping, or Performance Max (for YouTube, use DEMAND_GEN, not VIDEO). update renames it or changes its dates and network settings. status pauses or resumes it. budget sets the daily budget. bidding sets the bidding strategy. targeting sets location, language, and schedule.",
|
|
2484
|
-
limitations: "Every write previews first; then the user must type yes or ok in chat before confirmed:true + user_confirmation. New campaigns are created PAUSED. All edits here are in-place updates, not delete-and-recreate. Use the right action: update for name, dates, and networks; status for pause or resume; budget for the daily budget; bidding for the strategy; targeting for locations, languages, and schedule. Get campaign_id from report. Performance Max
|
|
2477
|
+
limitations: "Every write previews first; then the user must type yes or ok in chat before confirmed:true + user_confirmation. New campaigns are created PAUSED. All edits here are in-place updates, not delete-and-recreate. Use the right action: update for name, dates, and networks; status for pause or resume; budget for the daily budget; bidding for the strategy; targeting for locations, languages, and schedule. Get campaign_id from report. Performance Max and Demand Gen require automated bidding. App needs app_id, app_store, and TARGET_CPA bidding. Shopping needs a merchant_id from a linked Merchant Center account. VIDEO campaigns can no longer be created through the API (Google sunset Video Action campaigns for Demand Gen), so use DEMAND_GEN for YouTube instead. Location and language are numeric ids, so resolve names with google_ads_lookup first.",
|
|
2485
2478
|
examples: [
|
|
2486
2479
|
'{"action":"create","name":"Brand - Search","channel_type":"SEARCH","daily_budget":50,"bidding":"MAXIMIZE_CONVERSIONS"}',
|
|
2487
2480
|
'{"action":"create","name":"App - Installs","channel_type":"APP","daily_budget":50,"bidding":"TARGET_CPA","target_cpa":3,"app_id":"com.example.app","app_store":"GOOGLE_APP_STORE"}',
|
|
@@ -4260,7 +4253,7 @@ function registerOAuthRoutes(app, broker) {
|
|
|
4260
4253
|
|
|
4261
4254
|
// src/app.ts
|
|
4262
4255
|
async function startHttp(config, deps) {
|
|
4263
|
-
const { ads, audit, logger, store } = deps;
|
|
4256
|
+
const { ads, audit, logger, store, db } = deps;
|
|
4264
4257
|
const pageCache = new PageCache();
|
|
4265
4258
|
const oauthLimiter = new RateLimiter(60, 6e4);
|
|
4266
4259
|
const mcpLimiter = new RateLimiter(120, 6e4);
|
|
@@ -4291,6 +4284,68 @@ async function startHttp(config, deps) {
|
|
|
4291
4284
|
return remote || "unknown";
|
|
4292
4285
|
};
|
|
4293
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
|
+
});
|
|
4294
4349
|
app.use("/register", async (c, next) => {
|
|
4295
4350
|
if (!oauthLimiter.allow(`reg:${clientIp(c)}`)) return c.json({ error: "rate_limit", message: "Too many registration requests. Retry later." }, 429);
|
|
4296
4351
|
return next();
|
|
@@ -4476,7 +4531,7 @@ async function main() {
|
|
|
4476
4531
|
const db = openDb(config.auditDbPath);
|
|
4477
4532
|
const audit = new Audit(db, logger);
|
|
4478
4533
|
const store = new OAuthStore(db, loadEncKey(config.tokenEncKey));
|
|
4479
|
-
const running = await startHttp(config, { ads, audit, logger, store });
|
|
4534
|
+
const running = await startHttp(config, { ads, audit, logger, store, db });
|
|
4480
4535
|
const shutdown = (sig) => {
|
|
4481
4536
|
logger.info("main", `received ${sig}, shutting down`);
|
|
4482
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": {
|
|
@@ -51,22 +51,22 @@
|
|
|
51
51
|
"access": "public"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@hono/node-server": "^
|
|
54
|
+
"@hono/node-server": "^2.0.8",
|
|
55
55
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
56
|
-
"google-ads-api": "^
|
|
56
|
+
"google-ads-api": "^24.1.0",
|
|
57
57
|
"google-auth-library": "^9.15.0",
|
|
58
58
|
"hono": "^4.12.28",
|
|
59
59
|
"zod": "^4.4.3"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@eslint/js": "^10.0.1",
|
|
63
|
-
"@types/node": "^
|
|
63
|
+
"@types/node": "^26.1.1",
|
|
64
64
|
"eslint": "^10.6.0",
|
|
65
65
|
"globals": "^17.7.0",
|
|
66
66
|
"tsup": "^8.3.5",
|
|
67
67
|
"tsx": "^4.23.0",
|
|
68
|
-
"typescript": "
|
|
68
|
+
"typescript": "~6.0.3",
|
|
69
69
|
"typescript-eslint": "^8.63.0",
|
|
70
|
-
"vitest": "^
|
|
70
|
+
"vitest": "^4.1.10"
|
|
71
71
|
}
|
|
72
72
|
}
|