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 +21 -0
- package/README.md +263 -0
- package/config.example.json +26 -0
- package/dist/build-info.json +1 -0
- package/dist/errors.d.ts +17 -0
- package/dist/errors.js +117 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1751 -0
- package/dist/resilience.d.ts +3 -0
- package/dist/resilience.js +99 -0
- package/dist/tools.d.ts +2 -0
- package/dist/tools.js +574 -0
- package/package.json +71 -0
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"}
|
package/dist/errors.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED