@channel47/google-ads-mcp 1.0.2 → 1.0.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@channel47/google-ads-mcp",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Google Ads MCP Server - Query and mutate Google Ads data using GAQL",
5
5
  "main": "server/index.js",
6
6
  "bin": {
@@ -60,10 +60,14 @@ export async function mutate(params) {
60
60
  if (partial_failure && error.errors && Array.isArray(error.errors)) {
61
61
  for (const err of error.errors) {
62
62
  const opIndex = err.location?.field_path_elements?.[0]?.index ?? -1;
63
+ // Extract full field path for debugging (e.g., "operations.create.campaign_bidding_strategy")
64
+ const fieldPath = err.location?.field_path_elements?.map(e => e.field_name).join('.') || null;
63
65
  partialFailureErrors.push({
64
66
  message: err.message || JSON.stringify(err.error_code),
65
67
  error_code: err.error_code,
66
- operation_index: opIndex
68
+ operation_index: opIndex,
69
+ field_path: fieldPath,
70
+ trigger: err.trigger?.string_value || null
67
71
  });
68
72
  }
69
73
 
@@ -91,10 +95,13 @@ export async function mutate(params) {
91
95
  || [];
92
96
 
93
97
  for (const error of errorDetails) {
98
+ const fieldPath = error.location?.field_path_elements?.map(e => e.field_name).join('.') || null;
94
99
  partialFailureErrors.push({
95
100
  message: error.message || error.error_message || JSON.stringify(error),
96
101
  error_code: error.error_code || error.code,
97
- operation_index: error.location?.field_path_elements?.[0]?.index ?? -1
102
+ operation_index: error.location?.field_path_elements?.[0]?.index ?? -1,
103
+ field_path: fieldPath,
104
+ trigger: error.trigger?.string_value || null
98
105
  });
99
106
  }
100
107
  }
@@ -3,6 +3,15 @@
3
3
  * Converts standard Google Ads API format to Opteo library format
4
4
  */
5
5
 
6
+ /**
7
+ * Entities that legitimately use resource_name in CREATE operations.
8
+ * These use temporary resource IDs (-1, -2, etc.) for atomic multi-resource creation.
9
+ * See: https://developers.google.com/google-ads/api/docs/mutating/overview
10
+ */
11
+ const ENTITIES_REQUIRING_RESOURCE_NAME_IN_CREATE = new Set([
12
+ 'campaign_budget' // Used for temp IDs when creating budget + campaign atomically
13
+ ]);
14
+
6
15
  /**
7
16
  * Resource name URL path segments to entity type mapping
8
17
  * Based on Google Ads API resource name patterns
@@ -156,12 +165,13 @@ function transformToOpteoFormat(operation, index) {
156
165
 
157
166
  if (opType === 'remove') {
158
167
  // Remove operations have resource_name as string value
168
+ // The Opteo library expects resource to be the string directly for remove ops
159
169
  const resourceName = operation.remove;
160
170
  if (typeof resourceName !== 'string') {
161
171
  throw new Error(`Operation ${index}: 'remove' value must be a resource_name string`);
162
172
  }
163
173
  entity = inferEntityFromResourceName(resourceName);
164
- resource = { resource_name: resourceName };
174
+ resource = resourceName; // String, not object - Opteo expects { remove: "resource_name" }
165
175
  } else {
166
176
  // Create/Update operations have resource as object
167
177
  resource = operation[opType];
@@ -193,6 +203,16 @@ function transformToOpteoFormat(operation, index) {
193
203
  );
194
204
  }
195
205
 
206
+ // Strip resource_name from CREATE operations (API generates it automatically)
207
+ // Exception: entities that use temp IDs for atomic multi-resource creation
208
+ // See: https://developers.google.com/google-ads/api/docs/campaigns/create-campaigns
209
+ if (opType === 'create' && resource.resource_name) {
210
+ if (!ENTITIES_REQUIRING_RESOURCE_NAME_IN_CREATE.has(entity)) {
211
+ const { resource_name, ...resourceWithoutName } = resource;
212
+ resource = resourceWithoutName;
213
+ }
214
+ }
215
+
196
216
  return {
197
217
  entity,
198
218
  operation: opType,
@@ -214,8 +234,16 @@ export function normalizeOperations(operations) {
214
234
  const op = operations[i];
215
235
 
216
236
  if (isOpteoFormat(op)) {
217
- // Already in Opteo format - pass through
218
- normalizedOps.push(op);
237
+ // Already in Opteo format - pass through, but normalize remove operations
238
+ // For remove ops, ensure resource is the string, not an object
239
+ if (op.operation === 'remove' && op.resource && typeof op.resource === 'object') {
240
+ normalizedOps.push({
241
+ ...op,
242
+ resource: op.resource.resource_name || op.resource
243
+ });
244
+ } else {
245
+ normalizedOps.push(op);
246
+ }
219
247
  } else {
220
248
  // Transform from standard format
221
249
  const transformed = transformToOpteoFormat(op, i);