meta-ads-mcp 0.4.3__py3-none-any.whl → 0.4.5__py3-none-any.whl

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.
@@ -1,4 +1,4 @@
1
- """Callback server for Meta Ads API authentication and confirmations."""
1
+ """Callback server for Meta Ads API authentication."""
2
2
 
3
3
  import threading
4
4
  import socket
@@ -16,9 +16,6 @@ from .utils import logger
16
16
  # Global token container for communication between threads
17
17
  token_container = {"token": None, "expires_in": None, "user_id": None}
18
18
 
19
- # Global container for update confirmations
20
- update_confirmation = {"approved": False}
21
-
22
19
  # Global variables for server thread and state
23
20
  callback_server_thread = None
24
21
  callback_server_lock = threading.Lock()
@@ -41,16 +38,6 @@ class CallbackHandler(BaseHTTPRequestHandler):
41
38
  self._handle_oauth_callback()
42
39
  elif self.path.startswith("/token"):
43
40
  self._handle_token()
44
- elif self.path.startswith("/confirm-update"):
45
- self._handle_update_confirmation()
46
- elif self.path.startswith("/update-confirm"):
47
- self._handle_update_execution()
48
- elif self.path.startswith("/verify-update"):
49
- self._handle_update_verification()
50
- elif self.path.startswith("/api/adset"):
51
- self._handle_adset_api()
52
- elif self.path.startswith("/api/ad"):
53
- self._handle_ad_api()
54
41
  else:
55
42
  # If no matching path, return a 404 error
56
43
  self.send_response(404)
@@ -61,821 +48,97 @@ class CallbackHandler(BaseHTTPRequestHandler):
61
48
  self.end_headers()
62
49
 
63
50
  def _handle_oauth_callback(self):
64
- """Handle the OAuth callback from Meta"""
65
- self.send_response(200)
66
- self.send_header("Content-type", "text/html")
67
- self.end_headers()
68
-
69
- callback_html = """
70
- <!DOCTYPE html>
71
- <html>
72
- <head>
73
- <title>Authentication Successful</title>
74
- <style>
75
- body {
76
- font-family: Arial, sans-serif;
77
- line-height: 1.6;
78
- color: #333;
79
- max-width: 800px;
80
- margin: 0 auto;
81
- padding: 20px;
82
- }
83
- .success {
84
- color: #4CAF50;
85
- font-size: 24px;
86
- margin-bottom: 20px;
87
- }
88
- .info {
89
- background-color: #f5f5f5;
90
- padding: 15px;
91
- border-radius: 4px;
92
- margin-bottom: 20px;
93
- }
94
- .button {
95
- background-color: #4CAF50;
96
- color: white;
97
- padding: 10px 15px;
98
- border: none;
99
- border-radius: 4px;
100
- cursor: pointer;
101
- }
102
- </style>
103
- </head>
104
- <body>
105
- <div class="success">Authentication Successful!</div>
106
- <div class="info">
107
- <p>Your Meta Ads API token has been received.</p>
108
- <p>You can now close this window and return to the application.</p>
109
- </div>
110
- <button class="button" onclick="window.close()">Close Window</button>
111
-
112
- <script>
113
- // Function to parse URL parameters including fragments
114
- function parseURL(url) {
115
- var params = {};
116
- var parser = document.createElement('a');
117
- parser.href = url;
118
-
119
- // Parse fragment parameters
120
- var fragment = parser.hash.substring(1);
121
- var fragmentParams = fragment.split('&');
122
-
123
- for (var i = 0; i < fragmentParams.length; i++) {
124
- var pair = fragmentParams[i].split('=');
125
- params[pair[0]] = decodeURIComponent(pair[1]);
126
- }
127
-
128
- return params;
129
- }
130
-
131
- // Parse the URL to get the access token
132
- var params = parseURL(window.location.href);
133
- var token = params['access_token'];
134
- var expires_in = params['expires_in'];
135
-
136
- // Send the token to the server
137
- if (token) {
138
- // Create XMLHttpRequest object
139
- var xhr = new XMLHttpRequest();
140
-
141
- // Configure it to make a GET request to the /token endpoint
142
- xhr.open('GET', '/token?token=' + encodeURIComponent(token) +
143
- '&expires_in=' + encodeURIComponent(expires_in), true);
144
-
145
- // Set up a handler for when the request is complete
146
- xhr.onload = function() {
147
- if (xhr.status === 200) {
148
- console.log('Token successfully sent to server');
149
- } else {
150
- console.error('Failed to send token to server');
151
- }
152
- };
153
-
154
- // Send the request
155
- xhr.send();
156
- } else {
157
- console.error('No token found in URL');
158
- document.body.innerHTML += '<div style="color: red; margin-top: 20px;">Error: No authentication token found. Please try again.</div>';
159
- }
160
- </script>
161
- </body>
162
- </html>
163
- """
164
- self.wfile.write(callback_html.encode())
165
-
166
- def _handle_token(self):
167
- """Handle the token received from the callback"""
168
- # Extract token from query params
169
- query = parse_qs(urlparse(self.path).query)
170
- token_container["token"] = query.get("token", [""])[0]
171
-
172
- if "expires_in" in query:
173
- try:
174
- token_container["expires_in"] = int(query.get("expires_in", ["0"])[0])
175
- except ValueError:
176
- token_container["expires_in"] = None
177
-
178
- # Send success response
179
- self.send_response(200)
180
- self.send_header("Content-type", "text/plain")
181
- self.end_headers()
182
- self.wfile.write(b"Token received")
183
-
184
- # The actual token processing is now handled by the auth module
185
- # that imports this module and accesses token_container
186
-
187
- def _handle_update_confirmation(self):
188
- """Handle the update confirmation page"""
189
- # Extract query parameters
190
- query = parse_qs(urlparse(self.path).query)
191
- adset_id = query.get("adset_id", [""])[0]
192
- ad_id = query.get("ad_id", [""])[0]
193
- token = query.get("token", [""])[0]
194
- changes = query.get("changes", ["{}"])[0]
195
-
196
- # Determine what type of object we're updating
197
- object_id = ad_id if ad_id else adset_id
198
- object_type = "Ad" if ad_id else "Ad Set"
51
+ """Handle OAuth callback after user authorization"""
52
+ # Check if we're being redirected from Facebook with an authorization code
53
+ parsed_url = urlparse(self.path)
54
+ params = parse_qs(parsed_url.query)
199
55
 
200
- try:
201
- changes_dict = json.loads(changes)
202
- except json.JSONDecodeError:
203
- changes_dict = {}
56
+ # Check for code parameter
57
+ code = params.get('code', [None])[0]
58
+ state = params.get('state', [None])[0]
59
+ error = params.get('error', [None])[0]
204
60
 
205
- # Return confirmation page
61
+ # Send 200 OK response with a simple HTML page
206
62
  self.send_response(200)
207
- self.send_header("Content-type", "text/html; charset=utf-8")
208
- self.end_headers()
209
-
210
- # Create a properly escaped JSON string for JavaScript
211
- escaped_changes = json.dumps(changes).replace("'", "\\'").replace('"', '\\"')
212
-
213
- html = """
214
- <html>
215
- <head>
216
- <title>Confirm """ + object_type + """ Update</title>
217
- <meta charset="utf-8">
218
- <style>
219
- body { font-family: Arial, sans-serif; margin: 20px; max-width: 1000px; margin: 0 auto; }
220
- .warning { color: #d73a49; margin: 20px 0; padding: 15px; border-left: 4px solid #d73a49; background-color: #fff8f8; }
221
- .changes { background: #f6f8fa; padding: 15px; border-radius: 6px; }
222
- .buttons { margin-top: 20px; }
223
- button { padding: 10px 20px; margin-right: 10px; border-radius: 6px; cursor: pointer; }
224
- .approve { background: #2ea44f; color: white; border: none; }
225
- .cancel { background: #d73a49; color: white; border: none; }
226
- .diff-table { width: 100%; border-collapse: collapse; margin: 15px 0; }
227
- .diff-table td { padding: 8px; border: 1px solid #ddd; }
228
- .diff-table .header { background: #f1f8ff; font-weight: bold; }
229
- .status { padding: 15px; margin-top: 20px; border-radius: 6px; display: none; }
230
- .success { background-color: #e6ffed; border: 1px solid #2ea44f; color: #22863a; }
231
- .error { background-color: #ffeef0; border: 1px solid #d73a49; color: #d73a49; }
232
- pre { white-space: pre-wrap; word-break: break-all; }
233
- </style>
234
- </head>
235
- <body>
236
- <h1>Confirm """ + object_type + """ Update</h1>
237
- <p>You are about to update """ + object_type + """: <strong>""" + object_id + """</strong></p>
238
-
239
- <div class="warning">
240
- <p><strong>Warning:</strong> This action will directly update your """ + object_type.lower() + """ in Meta Ads. Please review the changes carefully before approving.</p>
241
- </div>
242
-
243
- <div class="changes">
244
- <h3>Changes to apply:</h3>
245
- <table class="diff-table">
246
- <tr class="header">
247
- <td>Field</td>
248
- <td>New Value</td>
249
- <td>Description</td>
250
- </tr>
251
- """
252
-
253
- # Generate table rows for each change
254
- for k, v in changes_dict.items():
255
- description = ""
256
- if k == "frequency_control_specs" and isinstance(v, list) and len(v) > 0:
257
- spec = v[0]
258
- if all(key in spec for key in ["event", "interval_days", "max_frequency"]):
259
- description = f"Cap to {spec['max_frequency']} {spec['event'].lower()} per {spec['interval_days']} days"
260
-
261
- # Special handling for targeting_automation
262
- elif k == "targeting" and isinstance(v, dict) and "targeting_automation" in v:
263
- targeting_auto = v.get("targeting_automation", {})
264
- if "advantage_audience" in targeting_auto:
265
- audience_value = targeting_auto["advantage_audience"]
266
- description = f"Set Advantage+ audience to {'ON' if audience_value == 1 else 'OFF'}"
267
- if audience_value == 1:
268
- description += " (may be restricted for Special Ad Categories)"
269
-
270
- # Format the value for display
271
- display_value = json.dumps(v, indent=2) if isinstance(v, (dict, list)) else str(v)
272
-
273
- html += f"""
274
- <tr>
275
- <td>{k}</td>
276
- <td><pre>{display_value}</pre></td>
277
- <td>{description}</td>
278
- </tr>
279
- """
280
-
281
- html += """
282
- </table>
283
- </div>
284
-
285
- <div class="buttons">
286
- <button class="approve" onclick="approveChanges()">Approve Changes</button>
287
- <button class="cancel" onclick="cancelChanges()">Cancel</button>
288
- </div>
289
-
290
- <div id="status" class="status"></div>
291
-
292
- <script>
293
- // Get the approve button
294
- const approveBtn = document.querySelector('.approve');
295
- const cancelBtn = document.querySelector('.cancel');
296
- const statusDiv = document.querySelector('.status');
297
-
298
- let debugMode = false;
299
- // Function to log debug messages
300
- function debugLog(...args) {
301
- if (debugMode) {
302
- console.log(...args);
303
- }
304
- }
305
-
306
- // Add event listeners to buttons
307
- approveBtn.addEventListener('click', approveChanges);
308
- cancelBtn.addEventListener('click', cancelChanges);
309
-
310
- function showStatus(message, isError = false) {
311
- statusDiv.textContent = message;
312
- statusDiv.style.display = 'block';
313
- statusDiv.className = 'status ' + (isError ? 'error' : 'success');
314
- }
315
-
316
- function approveChanges() {
317
- // Disable buttons to prevent double-submission
318
- const buttons = [approveBtn, cancelBtn];
319
- buttons.forEach(button => button.disabled = true);
320
-
321
- showStatus('Approving changes...');
322
-
323
- // Determine which object type we're updating
324
- const isAd = Boolean('""" + ad_id + """');
325
- const objectType = isAd ? 'ad' : 'adset';
326
- const objectId = '""" + object_id + """';
327
-
328
- // Create parameters
329
- const params = new URLSearchParams({
330
- action: 'approve',
331
- token: '""" + token + """',
332
- changes: '""" + escaped_changes + """'
333
- });
334
-
335
- // Add the appropriate ID parameter based on object type
336
- if (isAd) {
337
- params.append('ad_id', objectId);
338
- } else {
339
- params.append('adset_id', objectId);
340
- }
341
-
342
- debugLog("Sending update request with params", {
343
- objectType,
344
- objectId,
345
- changes: JSON.parse('""" + escaped_changes + """')
346
- });
347
-
348
- fetch('/update-confirm?' + params)
349
- .then(response => response.json())
350
- .then(data => {
351
- debugLog("Update response data", data);
352
- buttons.forEach(button => button.disabled = false);
353
-
354
- // Handle result
355
- if (data.status === 'error') {
356
- let errorMessage = data.error || 'Unknown error';
357
- const originalErrorDetails = data.api_error || {};
358
-
359
- showStatus('Error: ' + errorMessage, true);
360
-
361
- // Create a detailed error object for the verification page
362
- const fullErrorData = {
363
- message: errorMessage,
364
- details: data.errorDetails || [],
365
- apiError: originalErrorDetails || data.apiError || {},
366
- fullResponse: data.fullResponse || {}
367
- };
368
-
369
- debugLog("Redirecting with error message", errorMessage);
370
- debugLog("Full error data", fullErrorData);
371
-
372
- // Encode the stringified error object
373
- const encodedErrorData = encodeURIComponent(JSON.stringify(fullErrorData));
374
-
375
- // Redirect to verification page with detailed error information
376
- const errorParams = new URLSearchParams({
377
- token: '""" + token + """',
378
- error: errorMessage,
379
- errorData: encodedErrorData
380
- });
381
-
382
- // Add the appropriate ID parameter based on object type
383
- if (isAd) {
384
- errorParams.append('ad_id', objectId);
385
- } else {
386
- errorParams.append('adset_id', objectId);
387
- }
388
-
389
- window.location.href = '/verify-update?' + errorParams;
390
- } else {
391
- showStatus('Changes approved and will be applied shortly!');
392
- setTimeout(() => {
393
- const verifyParams = new URLSearchParams({
394
- token: '""" + token + """'
395
- });
396
-
397
- // Add the appropriate ID parameter based on object type
398
- if (isAd) {
399
- verifyParams.append('ad_id', objectId);
400
- } else {
401
- verifyParams.append('adset_id', objectId);
402
- }
403
-
404
- window.location.href = '/verify-update?' + verifyParams;
405
- }, 3000);
406
- }
407
- })
408
- .catch(error => {
409
- debugLog("Fetch error", error);
410
- showStatus('Error applying changes: ' + error, true);
411
- buttons.forEach(button => button.disabled = false);
412
- });
413
- }
414
-
415
- function cancelChanges() {
416
- showStatus("Cancelling update...");
417
-
418
- // Determine which object type we're updating
419
- const isAd = Boolean('""" + ad_id + """');
420
- const objectId = '""" + object_id + """';
421
-
422
- // Create parameters
423
- const params = new URLSearchParams({
424
- action: 'cancel'
425
- });
426
-
427
- // Add the appropriate ID parameter based on object type
428
- if (isAd) {
429
- params.append('ad_id', objectId);
430
- } else {
431
- params.append('adset_id', objectId);
432
- }
433
-
434
- fetch('/update-confirm?' + params)
435
- .then(() => {
436
- showStatus('Update cancelled.');
437
- setTimeout(() => window.close(), 2000);
438
- });
439
- }
440
- </script>
441
- </body>
442
- </html>
443
- """
444
- self.wfile.write(html.encode('utf-8'))
445
-
446
- def _handle_update_execution(self):
447
- """Handle the update execution after user confirmation"""
448
- # Handle update confirmation response
449
- query = parse_qs(urlparse(self.path).query)
450
- action = query.get("action", [""])[0]
451
-
452
- self.send_response(200)
453
- self.send_header("Content-type", "application/json")
63
+ self.send_header("Content-type", "text/html")
454
64
  self.end_headers()
455
65
 
456
- if action == "approve":
457
- adset_id = query.get("adset_id", [""])[0]
458
- ad_id = query.get("ad_id", [""])[0]
459
- token = query.get("token", [""])[0]
460
- changes = query.get("changes", ["{}"])[0]
461
-
462
- # Determine what type of object we're updating
463
- object_id = ad_id if ad_id else adset_id
464
- object_type = "ad" if ad_id else "adset"
465
-
466
- # Store the approval in the global variable for the main thread to process
467
- update_confirmation.clear()
468
- update_confirmation.update({
469
- "approved": True,
470
- "object_id": object_id,
471
- "object_type": object_type,
472
- "token": token,
473
- "changes": changes
66
+ if error:
67
+ # User denied access or other error occurred
68
+ html = f"""
69
+ <html>
70
+ <head><title>Authorization Failed</title></head>
71
+ <body>
72
+ <h1>Authorization Failed</h1>
73
+ <p>Error: {error}</p>
74
+ <p>The authorization was cancelled or failed. You can close this window.</p>
75
+ </body>
76
+ </html>
77
+ """
78
+ logger.error(f"OAuth authorization failed: {error}")
79
+ elif code:
80
+ # Success case - we have the authorization code
81
+ logger.info(f"Received authorization code: {code[:10]}...")
82
+
83
+ # Store the authorization code temporarily
84
+ # The auth module will exchange this for an access token
85
+ token_container.update({
86
+ "auth_code": code,
87
+ "state": state,
88
+ "timestamp": asyncio.get_event_loop().time()
474
89
  })
475
90
 
476
- # Process the update asynchronously
477
- result = asyncio.run(self._perform_update(object_id, token, changes))
478
- self.wfile.write(json.dumps(result).encode())
479
- else:
480
- # Store the cancellation
481
- update_confirmation.clear()
482
- update_confirmation.update({"approved": False})
483
- self.wfile.write(json.dumps({"status": "cancelled"}).encode())
484
-
485
- async def _perform_update(self, object_id, token, changes):
486
- """Perform the actual update of the adset or ad"""
487
- from .api import make_api_request
488
-
489
- try:
490
- # Handle potential multiple encoding of JSON
491
- decoded_changes = changes
492
- # Try multiple decode attempts to handle various encoding scenarios
493
- for _ in range(3): # Try up to 3 levels of decoding
494
- try:
495
- # Try to parse as JSON
496
- changes_obj = json.loads(decoded_changes)
497
- # If we got a string back, we need to decode again
498
- if isinstance(changes_obj, str):
499
- decoded_changes = changes_obj
500
- continue
501
- else:
502
- # We have a dictionary, break the loop
503
- changes_dict = changes_obj
504
- break
505
- except json.JSONDecodeError:
506
- # Try unescaping first
507
- import html
508
- decoded_changes = html.unescape(decoded_changes)
509
- try:
510
- changes_dict = json.loads(decoded_changes)
511
- break
512
- except:
513
- # Failed to parse, will try again in the next iteration
514
- pass
515
- else:
516
- # If we got here, we couldn't parse the JSON
517
- return {"status": "error", "error": f"Failed to decode changes JSON: {changes}"}
518
-
519
- endpoint = f"{object_id}"
520
-
521
- # Create API parameters properly
522
- api_params = {}
523
-
524
- # Add each change parameter
525
- for key, value in changes_dict.items():
526
- api_params[key] = value
527
-
528
- # Add the access token
529
- api_params["access_token"] = token
530
-
531
- # Log what we're about to send
532
- object_type = "ad set" if object_id.startswith("23") else "ad" # Simple heuristic based on ID prefix
533
- logger.info(f"Sending update to Meta API for {object_type} {object_id}")
534
- logger.info(f"Parameters: {json.dumps(api_params)}")
535
-
536
- # Make the API request to update the object
537
- result = await make_api_request(endpoint, token, api_params, method="POST")
538
-
539
- # Log the result
540
- logger.info(f"Meta API update result: {json.dumps(result) if isinstance(result, dict) else result}")
541
-
542
- # Handle various result formats
543
- if result is None:
544
- logger.error("Empty response from Meta API")
545
- return {"status": "error", "error": "Empty response from Meta API"}
546
-
547
- # Check if the result contains an error
548
- if isinstance(result, dict) and 'error' in result:
549
- # Extract detailed error message from Meta API
550
- error_obj = result['error']
551
- error_msg = error_obj.get('message', 'Unknown API error')
552
- detailed_error = ""
553
-
554
- # Check for more detailed error messages
555
- if 'error_user_msg' in error_obj and error_obj['error_user_msg']:
556
- detailed_error = error_obj['error_user_msg']
557
- logger.error(f"Meta API user-facing error message: {detailed_error}")
558
-
559
- # Extract error data if available
560
- error_specs = []
561
- if 'error_data' in error_obj and isinstance(error_obj['error_data'], str):
562
- try:
563
- error_data = json.loads(error_obj['error_data'])
564
- if 'blame_field_specs' in error_data and error_data['blame_field_specs']:
565
- blame_specs = error_data['blame_field_specs']
566
- if isinstance(blame_specs, list) and blame_specs:
567
- if isinstance(blame_specs[0], list):
568
- error_specs = [msg for msg in blame_specs[0] if msg]
569
- else:
570
- error_specs = [str(spec) for spec in blame_specs if spec]
571
-
572
- if error_specs:
573
- logger.error(f"Meta API blame field specs: {'; '.join(error_specs)}")
574
- except Exception as e:
575
- logger.error(f"Error parsing error_data: {e}")
576
-
577
- # Construct most descriptive error message
578
- if detailed_error:
579
- error_msg = detailed_error
580
- elif error_specs:
581
- error_msg = "; ".join(error_specs)
582
-
583
- # Log the detailed error information
584
- logger.error(f"Meta API error: {error_msg}")
585
- logger.error(f"Full error object: {json.dumps(error_obj)}")
586
-
587
- return {
588
- "status": "error",
589
- "error": error_msg,
590
- "api_error": result['error'],
591
- "detailed_errors": error_specs,
592
- "full_response": result
593
- }
594
-
595
- # Handle string results (which might be error messages)
596
- if isinstance(result, str):
597
- try:
598
- # Try to parse as JSON
599
- result_obj = json.loads(result)
600
- if isinstance(result_obj, dict) and 'error' in result_obj:
601
- return {"status": "error", "error": result_obj['error'].get('message', 'Unknown API error')}
602
- except:
603
- # If not parseable as JSON, return as error message
604
- return {"status": "error", "error": result}
605
-
606
- # If we got here, assume success
607
- return {"status": "approved", "api_result": result}
608
- except Exception as e:
609
- # Log the exception for debugging
610
- logger.error(f"Error in perform_update: {str(e)}")
611
- import traceback
612
- logger.error(traceback.format_exc())
613
- return {"status": "error", "error": str(e)}
614
-
615
- def _handle_update_verification(self):
616
- """Handle the verification page for updates"""
617
- query = parse_qs(urlparse(self.path).query)
618
- adset_id = query.get("adset_id", [""])[0]
619
- ad_id = query.get("ad_id", [""])[0]
620
- object_id = query.get("object_id", [""])[0]
621
-
622
- # For backward compatibility - use object_id if available, otherwise check for adset_id or ad_id
623
- if not object_id:
624
- object_id = ad_id if ad_id else adset_id
625
-
626
- object_type = query.get("object_type", [""])[0]
627
- if not object_type:
628
- object_type = "Ad" if ad_id else "Ad Set"
629
-
630
- token = query.get("token", [""])[0]
631
- error_message = query.get("error", [""])[0]
632
- error_data_encoded = query.get("errorData", [""])[0]
633
-
634
- # Respond with verification page
635
- self.send_response(200)
636
- self.send_header("Content-type", "text/html; charset=utf-8")
637
- self.end_headers()
638
-
639
- html = """
640
- <!DOCTYPE html>
641
- <html>
642
- <head>
643
- <title>Verification - """ + object_type + """ Update</title>
644
- <meta charset="utf-8">
645
- <style>
646
- body {
647
- font-family: Arial, sans-serif;
648
- margin: 20px;
649
- max-width: 1000px;
650
- margin: 0 auto;
651
- line-height: 1.6;
652
- }
653
- .header {
654
- margin-bottom: 20px;
655
- padding-bottom: 10px;
656
- border-bottom: 1px solid #ccc;
657
- }
658
- .success {
659
- color: #2ea44f;
660
- background-color: #e6ffed;
661
- padding: 15px;
662
- border-radius: 6px;
663
- margin: 20px 0;
664
- }
665
- .error {
666
- color: #d73a49;
667
- background-color: #ffeef0;
668
- padding: 15px;
669
- border-radius: 6px;
670
- margin: 20px 0;
671
- }
672
- .details {
673
- background: #f6f8fa;
674
- padding: 15px;
675
- border-radius: 6px;
676
- margin: 20px 0;
677
- }
678
- .btn {
679
- display: inline-block;
680
- padding: 10px 20px;
681
- background-color: #0366d6;
682
- color: white;
683
- border-radius: 6px;
684
- text-decoration: none;
685
- margin-top: 20px;
686
- }
687
- pre {
688
- white-space: pre-wrap;
689
- word-break: break-word;
690
- background: #f8f8f8;
691
- padding: 10px;
692
- border-radius: 4px;
693
- overflow: auto;
694
- }
695
- #currentDetails {
696
- display: none;
697
- margin-top: 20px;
698
- }
699
- .toggle-btn {
700
- background-color: #f1f8ff;
701
- border: 1px solid #c8e1ff;
702
- padding: 8px 16px;
703
- border-radius: 4px;
704
- cursor: pointer;
705
- margin-top: 20px;
706
- display: inline-block;
707
- }
708
- </style>
709
- </head>
710
- <body>
711
- <div class="header">
712
- <h1>""" + object_type + """ Update Verification</h1>
713
- <p>Object ID: <strong>""" + object_id + """</strong></p>
714
- </div>
715
- """
716
-
717
- # If there's an error message, display it
718
- if error_message:
719
- html += """
720
- <div class="error">
721
- <h3>❌ Update Failed</h3>
722
- <p><strong>Error:</strong> """ + error_message + """</p>
91
+ html = """
92
+ <html>
93
+ <head><title>Authorization Successful</title></head>
94
+ <body>
95
+ <h1>✅ Authorization Successful!</h1>
96
+ <p>You have successfully authorized the Meta Ads MCP application.</p>
97
+ <p>You can now close this window and return to your application.</p>
98
+ <script>
99
+ // Try to close the window automatically after 2 seconds
100
+ setTimeout(function() {
101
+ window.close();
102
+ }, 2000);
103
+ </script>
104
+ </body>
105
+ </html>
723
106
  """
724
-
725
- # If there's detailed error data, decode and display it
726
- if error_data_encoded:
727
- try:
728
- error_data = json.loads(urllib.parse.unquote(error_data_encoded))
729
- html += """
730
- <div class="details">
731
- <h4>Error Details:</h4>
732
- <pre>""" + json.dumps(error_data, indent=2) + """</pre>
733
- </div>
734
- """
735
- except:
736
- pass
737
-
738
- html += """</div>"""
107
+ logger.info("OAuth authorization successful")
739
108
  else:
740
- # No error, display success message
741
- html += """
742
- <div class="success">
743
- <h3>✅ Update Successful</h3>
744
- <p>Your """ + object_type.lower() + """ has been updated successfully.</p>
745
- </div>
109
+ # No code or error - something unexpected happened
110
+ html = """
111
+ <html>
112
+ <head><title>Unexpected Response</title></head>
113
+ <body>
114
+ <h1>Unexpected Response</h1>
115
+ <p>No authorization code or error received. Please try again.</p>
116
+ </body>
117
+ </html>
746
118
  """
119
+ logger.warning("OAuth callback received without code or error")
747
120
 
748
- # Add JavaScript to fetch the current details of the ad/adset
749
- html += """
750
- <button class="toggle-btn" id="toggleDetails">Show Current Details</button>
751
- <div id="currentDetails">
752
- <h3>Current """ + object_type + """ Details:</h3>
753
- <pre id="detailsJson">Loading...</pre>
754
- </div>
755
-
756
- <a href="#" class="btn" onclick="window.close()">Close Window</a>
757
-
758
- <script>
759
- document.getElementById('toggleDetails').addEventListener('click', function() {
760
- const detailsDiv = document.getElementById('currentDetails');
761
- const toggleBtn = document.getElementById('toggleDetails');
762
-
763
- if (detailsDiv.style.display === 'block') {
764
- detailsDiv.style.display = 'none';
765
- toggleBtn.textContent = 'Show Current Details';
766
- } else {
767
- detailsDiv.style.display = 'block';
768
- toggleBtn.textContent = 'Hide Current Details';
769
-
770
- // Fetch current details if not already loaded
771
- if (document.getElementById('detailsJson').textContent === 'Loading...') {
772
- fetchCurrentDetails();
773
- }
774
- }
775
- });
776
-
777
- function fetchCurrentDetails() {
778
- const objectId = '""" + object_id + """';
779
- const objectType = '""" + object_type.lower() + """';
780
- const endpoint = objectType === 'ad' ?
781
- `/api/ad?ad_id=${objectId}&token=""" + token + """` :
782
- `/api/adset?adset_id=${objectId}&token=""" + token + """`;
783
-
784
- fetch(endpoint)
785
- .then(response => response.json())
786
- .then(data => {
787
- document.getElementById('detailsJson').textContent = JSON.stringify(data, null, 2);
788
- })
789
- .catch(error => {
790
- document.getElementById('detailsJson').textContent = `Error fetching details: ${error}`;
791
- });
792
- }
793
- </script>
794
- </body>
795
- </html>
796
- """
797
-
798
- self.wfile.write(html.encode('utf-8'))
121
+ self.wfile.write(html.encode())
799
122
 
800
- def _handle_adset_api(self):
801
- """Handle API requests for adset data"""
802
- # Parse query parameters
803
- query = parse_qs(urlparse(self.path).query)
804
- adset_id = query.get("adset_id", [""])[0]
805
- token = query.get("token", [""])[0]
806
-
807
- from .api import make_api_request
808
-
809
- # Call the Graph API directly
810
- async def get_adset_data():
811
- try:
812
- endpoint = f"{adset_id}"
813
- params = {
814
- "fields": "id,name,campaign_id,status,daily_budget,lifetime_budget,targeting,bid_amount,bid_strategy,optimization_goal,billing_event,start_time,end_time,created_time,updated_time,attribution_spec,destination_type,promoted_object,pacing_type,budget_remaining,frequency_control_specs"
815
- }
816
-
817
- result = await make_api_request(endpoint, token, params)
818
-
819
- # Check if result is a string (possibly an error message)
820
- if isinstance(result, str):
821
- try:
822
- # Try to parse as JSON
823
- parsed_result = json.loads(result)
824
- return parsed_result
825
- except json.JSONDecodeError:
826
- # Return error object if can't parse as JSON
827
- return {"error": {"message": result}}
828
-
829
- # If the result is None, return an error object
830
- if result is None:
831
- return {"error": {"message": "Empty response from API"}}
832
-
833
- return result
834
- except Exception as e:
835
- logger.error(f"Error in get_adset_data: {str(e)}")
836
- return {"error": {"message": f"Error fetching ad set data: {str(e)}"}}
837
-
838
- # Run the async function
839
- loop = asyncio.new_event_loop()
840
- asyncio.set_event_loop(loop)
841
- result = loop.run_until_complete(get_adset_data())
842
- loop.close()
123
+ def _handle_token(self):
124
+ """Handle token endpoint for retrieving stored token data"""
125
+ # This endpoint allows other parts of the application to retrieve
126
+ # token information from the callback server
843
127
 
844
- # Return the result
845
128
  self.send_response(200)
846
129
  self.send_header("Content-type", "application/json")
847
130
  self.end_headers()
848
- self.wfile.write(json.dumps(result, indent=2).encode())
849
-
850
- def _handle_ad_api(self):
851
- """Handle API requests for ad data"""
852
- # Parse query parameters
853
- query = parse_qs(urlparse(self.path).query)
854
- ad_id = query.get("ad_id", [""])[0]
855
- token = query.get("token", [""])[0]
856
-
857
- from .api import make_api_request
858
131
 
859
- # Call the Graph API directly
860
- async def get_ad_data():
861
- endpoint = f"{ad_id}"
862
- params = {
863
- "fields": "id,name,adset_id,campaign_id,status,creative,created_time,updated_time,bid_amount,conversion_domain,tracking_specs,preview_shareable_link"
864
- }
865
- return await make_api_request(endpoint, token, params)
132
+ # Return current token container contents
133
+ response_data = {
134
+ "status": "success",
135
+ "data": token_container
136
+ }
866
137
 
867
- # Run the async function to get data
868
- result = asyncio.run(get_ad_data())
138
+ self.wfile.write(json.dumps(response_data).encode())
869
139
 
870
- # Send the response
871
- self.send_response(200)
872
- self.send_header("Content-type", "application/json")
873
- self.end_headers()
874
-
875
- if isinstance(result, dict):
876
- self.wfile.write(json.dumps(result, indent=2).encode())
877
- else:
878
- self.wfile.write(json.dumps({"error": "Failed to get ad data"}, indent=2).encode())
140
+ # The actual token processing is now handled by the auth module
141
+ # that imports this module and accesses token_container
879
142
 
880
143
  # Silence server logs
881
144
  def log_message(self, format, *args):
@@ -897,125 +160,98 @@ def shutdown_callback_server():
897
160
  server_shutdown_timer.cancel()
898
161
  server_shutdown_timer = None
899
162
 
900
- print(f"Shutting down callback server on port {callback_server_port}")
901
-
902
- # Shutdown the server if it exists
903
- if callback_server_instance:
904
- try:
163
+ try:
164
+ if callback_server_instance:
165
+ print("Shutting down callback server...")
905
166
  callback_server_instance.shutdown()
906
- callback_server_instance = None
907
- callback_server_running = False
908
- print("Callback server has been shut down")
909
- except Exception as e:
910
- print(f"Error shutting down callback server: {e}")
911
- else:
912
- print("No server instance to shut down")
167
+ callback_server_instance.server_close()
168
+ print("Callback server shut down successfully")
169
+
170
+ if callback_server_thread and callback_server_thread.is_alive():
171
+ callback_server_thread.join(timeout=5)
172
+ if callback_server_thread.is_alive():
173
+ print("Warning: Callback server thread did not shut down cleanly")
174
+ except Exception as e:
175
+ print(f"Error during callback server shutdown: {e}")
176
+ finally:
177
+ callback_server_running = False
178
+ callback_server_thread = None
179
+ callback_server_port = None
180
+ callback_server_instance = None
181
+
913
182
 
914
183
  def start_callback_server() -> int:
915
184
  """
916
- Start the callback server if it's not already running
185
+ Start the callback server and return the port number it's running on.
917
186
 
918
187
  Returns:
919
- Port number the server is running on
920
-
188
+ int: Port number the server is listening on
189
+
921
190
  Raises:
922
- Exception: If callback server is disabled via META_ADS_DISABLE_CALLBACK_SERVER environment variable
191
+ Exception: If the server fails to start
923
192
  """
924
- # Check if callback server is disabled via environment variable
925
- if os.environ.get("META_ADS_DISABLE_CALLBACK_SERVER"):
926
- logger.info("Callback server disabled via META_ADS_DISABLE_CALLBACK_SERVER environment variable")
927
- print("Callback server is disabled. OAuth authentication flow cannot be used.")
928
- raise Exception("Callback server disabled via META_ADS_DISABLE_CALLBACK_SERVER environment variable. Use alternative authentication methods.")
929
-
930
193
  global callback_server_thread, callback_server_running, callback_server_port, callback_server_instance, server_shutdown_timer
931
194
 
195
+ # Check if callback server is disabled
196
+ if os.environ.get("META_ADS_DISABLE_CALLBACK_SERVER"):
197
+ raise Exception("Callback server is disabled via META_ADS_DISABLE_CALLBACK_SERVER environment variable")
198
+
932
199
  with callback_server_lock:
933
200
  if callback_server_running:
934
201
  print(f"Callback server already running on port {callback_server_port}")
935
-
936
- # Reset the shutdown timer if one exists
937
- if server_shutdown_timer is not None:
938
- server_shutdown_timer.cancel()
939
-
940
- server_shutdown_timer = threading.Timer(CALLBACK_SERVER_TIMEOUT, shutdown_callback_server)
941
- server_shutdown_timer.daemon = True
942
- server_shutdown_timer.start()
943
- print(f"Reset server shutdown timer to {CALLBACK_SERVER_TIMEOUT} seconds")
944
-
945
202
  return callback_server_port
946
203
 
947
204
  # Find an available port
948
- port = 8888
205
+ port = 8080
949
206
  max_attempts = 10
950
207
  for attempt in range(max_attempts):
951
208
  try:
209
+ # Test if port is available
952
210
  with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
953
211
  s.bind(('localhost', port))
954
- break
212
+ break
955
213
  except OSError:
956
214
  port += 1
957
- if attempt == max_attempts - 1:
958
- raise Exception(f"Could not find an available port after {max_attempts} attempts")
215
+ else:
216
+ raise Exception(f"Could not find an available port after {max_attempts} attempts")
959
217
 
960
218
  callback_server_port = port
961
219
 
962
- try:
963
- # Create and start server in a daemon thread
964
- server = HTTPServer(('localhost', port), CallbackHandler)
965
- callback_server_instance = server
966
- print(f"Callback server starting on port {port}")
967
-
968
- # Create a simple flag to signal when the server is ready
969
- server_ready = threading.Event()
970
-
971
- def server_thread():
972
- try:
973
- # Signal that the server thread has started
974
- server_ready.set()
975
- print(f"Callback server is now ready on port {port}")
976
- # Start serving HTTP requests
977
- server.serve_forever()
978
- except Exception as e:
979
- print(f"Server error: {e}")
980
- finally:
981
- with callback_server_lock:
982
- global callback_server_running
983
- callback_server_running = False
984
-
985
- callback_server_thread = threading.Thread(target=server_thread)
986
- callback_server_thread.daemon = True
987
- callback_server_thread.start()
988
-
989
- # Wait for server to be ready (up to 5 seconds)
990
- if not server_ready.wait(timeout=5):
991
- print("Warning: Timeout waiting for server to start, but continuing anyway")
992
-
993
- callback_server_running = True
994
-
995
- # Set a timer to shutdown the server after CALLBACK_SERVER_TIMEOUT seconds
996
- if server_shutdown_timer is not None:
997
- server_shutdown_timer.cancel()
998
-
999
- server_shutdown_timer = threading.Timer(CALLBACK_SERVER_TIMEOUT, shutdown_callback_server)
1000
- server_shutdown_timer.daemon = True
1001
- server_shutdown_timer.start()
1002
- print(f"Server will automatically shut down after {CALLBACK_SERVER_TIMEOUT} seconds of inactivity")
1003
-
1004
- # Verify the server is actually accepting connections
1005
- try:
1006
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
1007
- s.settimeout(2)
1008
- s.connect(('localhost', port))
1009
- print(f"Confirmed server is accepting connections on port {port}")
1010
- except Exception as e:
1011
- print(f"Warning: Could not verify server connection: {e}")
1012
-
1013
- return port
1014
-
1015
- except Exception as e:
1016
- print(f"Error starting callback server: {e}")
1017
- # Try again with a different port in case of bind issues
1018
- if "address already in use" in str(e).lower():
1019
- print("Port may be in use, trying a different port...")
1020
- return start_callback_server() # Recursive call with a new port
1021
- raise e
220
+ # Start the server in a separate thread
221
+ callback_server_thread = threading.Thread(target=server_thread, daemon=True)
222
+ callback_server_thread.start()
223
+
224
+ # Wait a moment for the server to start
225
+ import time
226
+ time.sleep(0.5)
227
+
228
+ if not callback_server_running:
229
+ raise Exception("Failed to start callback server")
230
+
231
+ # Set up automatic shutdown timer
232
+ def auto_shutdown():
233
+ print(f"Callback server auto-shutdown after {CALLBACK_SERVER_TIMEOUT} seconds")
234
+ shutdown_callback_server()
235
+
236
+ server_shutdown_timer = threading.Timer(CALLBACK_SERVER_TIMEOUT, auto_shutdown)
237
+ server_shutdown_timer.start()
238
+
239
+ print(f"Callback server started on http://localhost:{port}")
240
+ return port
241
+
242
+
243
+ def server_thread():
244
+ """Thread function to run the callback server"""
245
+ global callback_server_running, callback_server_instance
246
+
247
+ try:
248
+ callback_server_instance = HTTPServer(('localhost', callback_server_port), CallbackHandler)
249
+ callback_server_running = True
250
+ print(f"Callback server thread started on port {callback_server_port}")
251
+ callback_server_instance.serve_forever()
252
+ except Exception as e:
253
+ print(f"Callback server error: {e}")
254
+ callback_server_running = False
255
+ finally:
256
+ print("Callback server thread finished")
257
+ callback_server_running = False