@channel47/google-ads-mcp 1.0.0 → 1.0.1

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.0",
3
+ "version": "1.0.1",
4
4
  "description": "Google Ads MCP Server - Query and mutate Google Ads data using GAQL",
5
5
  "main": "server/index.js",
6
6
  "bin": {
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { getCustomerClient } from '../auth.js';
3
+ import { formatSuccess, formatError } from '../utils/response-format.js';
3
4
 
4
5
  /**
5
6
  * Execute mutation operations using GoogleAdsService.Mutate
@@ -22,43 +23,105 @@ export async function mutate(params) {
22
23
 
23
24
  // Validate required parameters
24
25
  if (!customer_id) {
25
- return {
26
- success: false,
27
- error: 'customer_id is required (either as parameter or GOOGLE_ADS_CUSTOMER_ID env var)'
28
- };
26
+ return formatError(new Error('customer_id is required (either as parameter or GOOGLE_ADS_CUSTOMER_ID env var)'));
29
27
  }
30
28
 
31
29
  if (!operations || !Array.isArray(operations) || operations.length === 0) {
32
- return {
33
- success: false,
34
- error: 'operations array is required and must contain at least one operation'
35
- };
30
+ return formatError(new Error('operations array is required and must contain at least one operation'));
36
31
  }
37
32
 
38
- try {
39
- const customer = getCustomerClient(customer_id);
33
+ const customer = getCustomerClient(customer_id);
34
+ let response;
35
+ let partialFailureErrors = [];
40
36
 
37
+ try {
41
38
  // Execute mutation with validation options
42
- const response = await customer.mutateResources(operations, {
39
+ response = await customer.mutateResources(operations, {
43
40
  partialFailure: partial_failure,
44
41
  validateOnly: dry_run
45
42
  });
46
-
47
- return {
48
- success: true,
49
- dry_run,
50
- results: response,
51
- message: dry_run
52
- ? 'Validation successful - no changes made'
53
- : 'Mutations applied successfully',
54
- operations_count: operations.length
55
- };
56
43
  } catch (error) {
44
+ // The Opteo library throws exceptions with error.errors array for partial failures
45
+ // Extract failure details if available
46
+ if (partial_failure && error.errors && Array.isArray(error.errors)) {
47
+ for (const err of error.errors) {
48
+ const opIndex = err.location?.field_path_elements?.[0]?.index ?? -1;
49
+ partialFailureErrors.push({
50
+ message: err.message || JSON.stringify(err.error_code),
51
+ error_code: err.error_code,
52
+ operation_index: opIndex
53
+ });
54
+ }
55
+
56
+ // If not all operations failed, treat as partial success
57
+ if (partialFailureErrors.length < operations.length) {
58
+ response = { mutate_operation_responses: [], partial_failure_error: null };
59
+ } else {
60
+ // All operations failed
61
+ const errorMessages = partialFailureErrors.map(e => e.message).join('; ');
62
+ return formatError(new Error(`All operations failed: ${errorMessages}`));
63
+ }
64
+ } else {
65
+ // Not a partial failure - re-throw as regular error
66
+ return formatError(error);
67
+ }
68
+ }
69
+
70
+ // Extract results from response
71
+ const results = response.mutate_operation_responses || [];
72
+
73
+ // Check for partial failure errors in response body (alternative structure)
74
+ if (response.partial_failure_error) {
75
+ const errorDetails = response.partial_failure_error.errors
76
+ || response.partial_failure_error.details
77
+ || [];
78
+
79
+ for (const error of errorDetails) {
80
+ partialFailureErrors.push({
81
+ message: error.message || error.error_message || JSON.stringify(error),
82
+ error_code: error.error_code || error.code,
83
+ operation_index: error.location?.field_path_elements?.[0]?.index ?? -1
84
+ });
85
+ }
86
+ }
87
+
88
+ // Calculate success/failure counts
89
+ const failCount = partialFailureErrors.length;
90
+ const successCount = operations.length - failCount;
91
+
92
+ // Extract resource names from nested result structure
93
+ // Results come as: { campaign_result: { resource_name: "..." }, response: "campaign_result" }
94
+ const extractedResults = results.map(r => {
95
+ // Find the result field (campaign_result, ad_group_result, etc.)
96
+ const resultKey = r?.response;
97
+ const resultData = resultKey ? r[resultKey] : r;
57
98
  return {
58
- success: false,
59
- dry_run,
60
- error: error.message,
61
- error_details: error.errors || null
99
+ resource_name: resultData?.resource_name || null
62
100
  };
101
+ });
102
+
103
+ // Build appropriate message
104
+ let message;
105
+ if (dry_run) {
106
+ message = failCount > 0
107
+ ? `Validation completed with ${failCount} error(s) - no changes made`
108
+ : 'Validation successful - no changes made';
109
+ } else {
110
+ message = failCount > 0
111
+ ? `Mutations completed: ${successCount} succeeded, ${failCount} failed`
112
+ : 'Mutations applied successfully';
63
113
  }
114
+
115
+ return formatSuccess({
116
+ summary: `${message} (${operations.length} operation${operations.length !== 1 ? 's' : ''})`,
117
+ data: extractedResults,
118
+ metadata: {
119
+ dry_run,
120
+ operations_count: operations.length,
121
+ success_count: successCount,
122
+ failure_count: failCount,
123
+ customer_id,
124
+ ...(partialFailureErrors.length > 0 && { errors: partialFailureErrors })
125
+ }
126
+ });
64
127
  }