mcp-google-ads 1.0.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 drak-marketing
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,263 @@
1
+ # MCP Google Ads Server
2
+
3
+ An MCP (Model Context Protocol) server for the Google Ads API with built-in safeguards for review before changes go live. Production-proven with MCC (Manager Account) support, 35 tools for campaign management, reporting, and optimization.
4
+
5
+ ## Features
6
+
7
+ - **MCC Support**: Works with Manager accounts and multiple client accounts
8
+ - **Auto-Context**: Detects which client account based on your working directory
9
+ - **Safe by Default**: All new items created in PAUSED state
10
+ - **Approval Workflow**: Enable items only after manual review
11
+ - **Validation**: Validates ads before creating to catch errors early
12
+ - **Resilience**: Circuit breakers, retry with backoff, and timeout handling (cockatiel)
13
+ - **Structured Logging**: Pino-based logging with build fingerprinting
14
+
15
+ ## Setup
16
+
17
+ ### 1. Google Ads API Access
18
+
19
+ You need:
20
+ - A Google Ads **Developer Token** (apply at [Google Ads API Center](https://developers.google.com/google-ads/api/docs/get-started/dev-token))
21
+ - **OAuth credentials** (Client ID & Secret from Google Cloud Console)
22
+ - A **Refresh Token** for your MCC account
23
+
24
+ #### Getting OAuth Credentials
25
+
26
+ 1. Go to [Google Cloud Console](https://console.cloud.google.com/)
27
+ 2. Create a project or select existing
28
+ 3. Enable the **Google Ads API**
29
+ 4. Go to **Credentials** → **Create Credentials** → **OAuth Client ID**
30
+ 5. Choose **Desktop App**
31
+ 6. Download the JSON (contains client_id and client_secret)
32
+
33
+ #### Getting a Refresh Token
34
+
35
+ Use the Google OAuth playground or run:
36
+
37
+ ```bash
38
+ pip install google-ads
39
+ google-ads-auth
40
+ ```
41
+
42
+ ### 2. Install
43
+
44
+ ```bash
45
+ npm install mcp-google-ads
46
+ ```
47
+
48
+ Or clone and build from source:
49
+
50
+ ```bash
51
+ git clone https://github.com/mharnett/mcp-google-ads.git
52
+ cd mcp-google-ads
53
+ npm install
54
+ npm run build
55
+ ```
56
+
57
+ ### 3. Configure
58
+
59
+ ```bash
60
+ cp config.example.json config.json
61
+ ```
62
+
63
+ Edit `config.json` with your credentials:
64
+
65
+ ```json
66
+ {
67
+ "google_ads": {
68
+ "developer_token": "YOUR_DEVELOPER_TOKEN",
69
+ "client_id": "YOUR_CLIENT_ID.apps.googleusercontent.com",
70
+ "client_secret": "YOUR_CLIENT_SECRET",
71
+ "refresh_token": "YOUR_REFRESH_TOKEN",
72
+ "mcc_customer_id": "123-456-7890"
73
+ },
74
+ "clients": {
75
+ "my-client": {
76
+ "customer_id": "111-222-3333",
77
+ "name": "My Client",
78
+ "folder": "/path/to/client/workspace"
79
+ },
80
+ "another-client": {
81
+ "customer_id": "444-555-6666",
82
+ "name": "Another Client",
83
+ "folder": "/path/to/another/workspace"
84
+ }
85
+ },
86
+ "defaults": {
87
+ "create_paused": true,
88
+ "label_prefix": "claude-",
89
+ "require_approval_for_enable": true
90
+ }
91
+ }
92
+ ```
93
+
94
+ ### 4. Add to Claude Code
95
+
96
+ Add to your Claude Code MCP settings (`~/.claude/settings.json` or project settings):
97
+
98
+ ```json
99
+ {
100
+ "mcpServers": {
101
+ "google-ads": {
102
+ "command": "node",
103
+ "args": ["node_modules/mcp-google-ads/dist/index.js"]
104
+ }
105
+ }
106
+ }
107
+ ```
108
+
109
+ Or if installed from source:
110
+
111
+ ```json
112
+ {
113
+ "mcpServers": {
114
+ "google-ads": {
115
+ "command": "node",
116
+ "args": ["/path/to/mcp-google-ads/dist/index.js"]
117
+ }
118
+ }
119
+ }
120
+ ```
121
+
122
+ Restart Claude Code.
123
+
124
+ ## Usage
125
+
126
+ ### Workflow
127
+
128
+ ```
129
+ 1. cd into client folder → auto-detects account context
130
+ 2. Ask Claude to create campaigns/ads → all created PAUSED
131
+ 3. Review in Google Ads UI or Editor
132
+ 4. Tell Claude to enable approved items
133
+ 5. Claude enables (requires your approval prompt)
134
+ ```
135
+
136
+ ### Available Tools (35)
137
+
138
+ #### Context & Discovery
139
+ | Tool | Description |
140
+ |------|-------------|
141
+ | `google_ads_get_client_context` | Detect which account from working directory |
142
+ | `google_ads_list_campaigns` | List all campaigns with status and metrics |
143
+ | `google_ads_list_ad_groups` | List ad groups in a campaign |
144
+ | `google_ads_list_pending_changes` | Show paused items with claude- label |
145
+ | `google_ads_list_conversion_actions` | List conversion actions |
146
+
147
+ #### Campaign Management
148
+ | Tool | Description |
149
+ |------|-------------|
150
+ | `google_ads_create_campaign` | Create campaign (PAUSED) |
151
+ | `google_ads_create_ad_group` | Create ad group (PAUSED) |
152
+ | `google_ads_create_responsive_search_ad` | Create RSA with validation (PAUSED) |
153
+ | `google_ads_create_keywords` | Create keywords (PAUSED) |
154
+ | `google_ads_validate_ad` | Validate RSA without creating |
155
+ | `google_ads_enable_items` | Enable items (make LIVE) — **requires approval** |
156
+ | `google_ads_pause_items` | Pause active items |
157
+ | `google_ads_pause_keywords` | Pause specific keywords |
158
+ | `google_ads_update_campaign_budget` | Update campaign daily budget |
159
+
160
+ #### Tracking & URLs
161
+ | Tool | Description |
162
+ |------|-------------|
163
+ | `google_ads_get_campaign_tracking` | Get tracking templates and URL parameters |
164
+ | `google_ads_update_campaign_tracking` | Update tracking templates |
165
+
166
+ #### Negative Keywords
167
+ | Tool | Description |
168
+ |------|-------------|
169
+ | `google_ads_create_shared_set` | Create shared negative keyword list |
170
+ | `google_ads_link_shared_set` | Link shared set to campaign |
171
+ | `google_ads_unlink_shared_set` | Unlink shared set from campaign |
172
+ | `google_ads_add_shared_negatives` | Add keywords to shared negative list |
173
+ | `google_ads_remove_shared_negatives` | Remove keywords from shared list |
174
+ | `google_ads_add_campaign_negatives` | Add campaign-level negatives |
175
+ | `google_ads_remove_campaign_negatives` | Remove campaign-level negatives |
176
+ | `google_ads_remove_adgroup_negatives` | Remove ad group-level negatives |
177
+
178
+ #### Performance & Reporting
179
+ | Tool | Description |
180
+ |------|-------------|
181
+ | `google_ads_keyword_performance` | Keyword metrics with quality score |
182
+ | `google_ads_keyword_performance_by_conversion` | Keyword metrics by conversion action |
183
+ | `google_ads_ad_performance` | Ad-level performance metrics |
184
+ | `google_ads_ad_performance_by_conversion` | Ad metrics by conversion action |
185
+ | `google_ads_search_term_report` | Search term query report |
186
+ | `google_ads_search_term_report_by_conversion` | Search terms by conversion action |
187
+ | `google_ads_search_term_insights` | Search term category insights |
188
+ | `google_ads_search_term_insight_terms` | Terms within insight categories |
189
+ | `google_ads_keyword_volume` | Keyword planner volume estimates |
190
+
191
+ #### Advanced
192
+ | Tool | Description |
193
+ |------|-------------|
194
+ | `google_ads_gaql_query` | Run raw GAQL queries |
195
+
196
+ ### Example Commands
197
+
198
+ ```
199
+ # Check which account you're working with
200
+ "What Google Ads account am I connected to?"
201
+
202
+ # List campaigns
203
+ "Show me all campaigns in this account"
204
+
205
+ # Create a new campaign
206
+ "Create a Search campaign for brand terms with $50/day budget"
207
+
208
+ # Check what's pending review
209
+ "What changes are pending my review?"
210
+
211
+ # After reviewing in Google Ads UI
212
+ "Enable the approved ads in the Brand campaign"
213
+
214
+ # Performance analysis
215
+ "Show me keyword performance for the last 30 days, sorted by cost"
216
+
217
+ # Run custom GAQL
218
+ "Run a GAQL query to get all ad groups with CTR below 2%"
219
+ ```
220
+
221
+ ## Safety Features
222
+
223
+ 1. **Everything starts PAUSED** — Nothing goes live until you explicitly enable it
224
+ 2. **Label tracking** — All Claude-created items get a `claude-pending` label
225
+ 3. **Validation** — Ads are validated before creation (headline/description lengths, etc.)
226
+ 4. **Approval prompts** — The `enable_items` tool requires explicit approval in Claude Code
227
+ 5. **Client isolation** — Working directory determines which account, preventing cross-client mistakes
228
+
229
+ ## Adding New Clients
230
+
231
+ Edit `config.json` to add clients. Map each client to a working directory:
232
+
233
+ ```json
234
+ {
235
+ "clients": {
236
+ "client-slug": {
237
+ "customer_id": "123-456-7890",
238
+ "name": "Client Name",
239
+ "folder": "/path/to/client/workspace"
240
+ }
241
+ }
242
+ }
243
+ ```
244
+
245
+ No server restart needed — config is read on each request.
246
+
247
+ ## Troubleshooting
248
+
249
+ ### "No client found for working directory"
250
+ - Make sure you're in a folder that matches one of your `clients` entries
251
+ - Check that the folder path in config.json matches exactly
252
+
253
+ ### "Developer token not approved"
254
+ - New developer tokens need approval from Google
255
+ - Use a test account while waiting for approval
256
+
257
+ ### "Authentication failed"
258
+ - Refresh token may be expired — regenerate it
259
+ - Check that client_id and client_secret are correct
260
+
261
+ ## License
262
+
263
+ MIT — see [LICENSE](LICENSE) for details.
@@ -0,0 +1,26 @@
1
+ {
2
+ "google_ads": {
3
+ "developer_token": "YOUR_DEVELOPER_TOKEN",
4
+ "client_id": "YOUR_CLIENT_ID.apps.googleusercontent.com",
5
+ "client_secret": "YOUR_CLIENT_SECRET",
6
+ "refresh_token": "YOUR_REFRESH_TOKEN",
7
+ "mcc_customer_id": "123-456-7890"
8
+ },
9
+ "clients": {
10
+ "my-client": {
11
+ "customer_id": "111-222-3333",
12
+ "name": "My Client",
13
+ "folder": "/path/to/client/workspace"
14
+ },
15
+ "another-client": {
16
+ "customer_id": "444-555-6666",
17
+ "name": "Another Client",
18
+ "folder": "/path/to/another/workspace"
19
+ }
20
+ },
21
+ "defaults": {
22
+ "create_paused": true,
23
+ "label_prefix": "claude-",
24
+ "require_approval_for_enable": true
25
+ }
26
+ }
@@ -0,0 +1 @@
1
+ {"sha":"908c0fe","builtAt":"2026-03-30T21:48:20.831Z"}
@@ -0,0 +1,17 @@
1
+ export declare class GoogleAdsAuthError extends Error {
2
+ readonly cause?: unknown | undefined;
3
+ constructor(message: string, cause?: unknown | undefined);
4
+ }
5
+ export declare class GoogleAdsRateLimitError extends Error {
6
+ readonly retryAfterMs: number;
7
+ constructor(retryAfterMs: number, cause?: unknown);
8
+ }
9
+ export declare class GoogleAdsServiceError extends Error {
10
+ readonly cause?: unknown | undefined;
11
+ constructor(message: string, cause?: unknown | undefined);
12
+ }
13
+ export declare function validateCredentials(): {
14
+ valid: boolean;
15
+ missing: string[];
16
+ };
17
+ export declare function classifyError(error: any): Error;
package/dist/errors.js ADDED
@@ -0,0 +1,117 @@
1
+ // ============================================
2
+ // TYPED ERRORS (mirrors motion-mcp pattern)
3
+ // ============================================
4
+ export class GoogleAdsAuthError extends Error {
5
+ cause;
6
+ constructor(message, cause) {
7
+ super(message);
8
+ this.cause = cause;
9
+ this.name = "GoogleAdsAuthError";
10
+ }
11
+ }
12
+ export class GoogleAdsRateLimitError extends Error {
13
+ retryAfterMs;
14
+ constructor(retryAfterMs, cause) {
15
+ super(`Rate limited, retry after ${retryAfterMs}ms`);
16
+ this.retryAfterMs = retryAfterMs;
17
+ this.name = "GoogleAdsRateLimitError";
18
+ this.cause = cause;
19
+ }
20
+ }
21
+ export class GoogleAdsServiceError extends Error {
22
+ cause;
23
+ constructor(message, cause) {
24
+ super(message);
25
+ this.cause = cause;
26
+ this.name = "GoogleAdsServiceError";
27
+ }
28
+ }
29
+ // ============================================
30
+ // STARTUP CREDENTIAL VALIDATION
31
+ // ============================================
32
+ export function validateCredentials() {
33
+ const required = [
34
+ "GOOGLE_ADS_DEVELOPER_TOKEN",
35
+ "GOOGLE_ADS_CLIENT_ID",
36
+ "GOOGLE_ADS_CLIENT_SECRET",
37
+ "GOOGLE_ADS_REFRESH_TOKEN",
38
+ ];
39
+ const missing = required.filter((key) => !process.env[key] || process.env[key].trim() === "");
40
+ return { valid: missing.length === 0, missing };
41
+ }
42
+ /**
43
+ * Extract a human-readable message from a Google Ads API / gRPC error.
44
+ * The google-ads-api library throws objects where the real message lives
45
+ * in `error.errors[0].message`, not on the top-level `.message` property.
46
+ */
47
+ function extractErrorMessage(error) {
48
+ // 1. Nested errors array (google-ads-api gRPC errors)
49
+ if (Array.isArray(error?.errors) && error.errors.length > 0) {
50
+ const nested = error.errors[0];
51
+ if (typeof nested?.message === "string" && nested.message) {
52
+ return nested.message;
53
+ }
54
+ // Some errors have error_code as an object like { query_error: 'UNRECOGNIZED_FIELD' }
55
+ if (nested?.error_code) {
56
+ const codeEntries = Object.entries(nested.error_code).filter(([, v]) => v !== 0 && v !== "UNSPECIFIED");
57
+ if (codeEntries.length > 0) {
58
+ return codeEntries.map(([k, v]) => `${k}: ${v}`).join(", ");
59
+ }
60
+ }
61
+ }
62
+ // 2. Top-level message (if it's a real string, not "[object Object]")
63
+ if (typeof error?.message === "string" && error.message && !error.message.includes("[object Object]")) {
64
+ return error.message;
65
+ }
66
+ // 3. Fallback: try JSON serialization for useful output
67
+ try {
68
+ const json = JSON.stringify(error, null, 0);
69
+ if (json && json !== "{}" && json.length < 500) {
70
+ return json;
71
+ }
72
+ }
73
+ catch {
74
+ // ignore
75
+ }
76
+ return String(error);
77
+ }
78
+ export function classifyError(error) {
79
+ const message = extractErrorMessage(error);
80
+ const code = error?.errors?.[0]?.error_code;
81
+ const status = error?.status;
82
+ // Auth failures: expired tokens, invalid credentials, permission denied
83
+ if (status === 401 ||
84
+ status === 403 ||
85
+ message.includes("AUTHENTICATION_ERROR") ||
86
+ message.includes("AUTHORIZATION_ERROR") ||
87
+ message.includes("invalid_grant") ||
88
+ message.includes("Token has been expired") ||
89
+ message.includes("refresh token") ||
90
+ code?.authentication_error ||
91
+ code?.authorization_error) {
92
+ return new GoogleAdsAuthError(`Auth failed: ${message}. Check your refresh token and OAuth credentials.`, error);
93
+ }
94
+ // Rate limiting
95
+ if (status === 429 ||
96
+ message.includes("RESOURCE_EXHAUSTED") ||
97
+ code?.quota_error) {
98
+ const retryMs = error?.retryAfter ? error.retryAfter * 1000 : 60_000;
99
+ return new GoogleAdsRateLimitError(retryMs, error);
100
+ }
101
+ // Server errors
102
+ if (status >= 500 || message.includes("INTERNAL_ERROR")) {
103
+ return new GoogleAdsServiceError(`Google Ads API server error: ${message}`, error);
104
+ }
105
+ // Unclassified: wrap in a proper Error so .message is always a string
106
+ if (!(error instanceof Error)) {
107
+ const wrapped = new Error(message);
108
+ wrapped.name = "GoogleAdsError";
109
+ wrapped.cause = error;
110
+ return wrapped;
111
+ }
112
+ // If original error.message was "[object Object]", replace it
113
+ if (error.message.includes("[object Object]")) {
114
+ error.message = message;
115
+ }
116
+ return error;
117
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};