meta-ads-mcp 0.2.4__py3-none-any.whl → 0.2.6__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.
meta_ads_mcp/core/auth.py CHANGED
@@ -5,14 +5,18 @@ import time
5
5
  import platform
6
6
  import pathlib
7
7
  import os
8
- import threading
9
- import socket
10
8
  import webbrowser
11
9
  import asyncio
12
- from urllib.parse import urlparse, parse_qs
13
- from http.server import HTTPServer, BaseHTTPRequestHandler
14
10
  import json
15
11
  from .utils import logger
12
+ import requests
13
+
14
+ # Import from the new callback server module
15
+ from .callback_server import (
16
+ start_callback_server,
17
+ token_container,
18
+ update_confirmation
19
+ )
16
20
 
17
21
  # Auth constants
18
22
  AUTH_SCOPE = "ads_management,ads_read,business_management"
@@ -24,21 +28,9 @@ logger.info("Authentication module initialized")
24
28
  logger.info(f"Auth scope: {AUTH_SCOPE}")
25
29
  logger.info(f"Default redirect URI: {AUTH_REDIRECT_URI}")
26
30
 
27
- # Global token container for communication between threads
28
- token_container = {"token": None, "expires_in": None, "user_id": None}
29
-
30
- # Global container for update confirmations
31
- update_confirmation = {"approved": False}
32
-
33
31
  # Global flag for authentication state
34
32
  needs_authentication = False
35
33
 
36
- # Global variable for server thread and state
37
- callback_server_thread = None
38
- callback_server_lock = threading.Lock()
39
- callback_server_running = False
40
- callback_server_port = None
41
-
42
34
  # Meta configuration singleton
43
35
  class MetaConfig:
44
36
  _instance = None
@@ -213,6 +205,9 @@ class AuthManager:
213
205
  # Start the callback server if not already running
214
206
  port = start_callback_server()
215
207
 
208
+ # Update redirect URI with the actual port
209
+ self.redirect_uri = f"http://localhost:{port}/callback"
210
+
216
211
  # Generate the auth URL
217
212
  auth_url = self.get_auth_url()
218
213
 
@@ -261,535 +256,123 @@ class AuthManager:
261
256
  self.invalidate_token()
262
257
 
263
258
 
264
- # Callback Handler class definition
265
- class CallbackHandler(BaseHTTPRequestHandler):
266
- def do_GET(self):
267
- global token_container, auth_manager, needs_authentication
259
+ def process_token_response(token_container):
260
+ """Process the token response from Facebook."""
261
+ global needs_authentication, auth_manager
262
+
263
+ if token_container and token_container.get('token'):
264
+ logger.info("Processing token response from Facebook OAuth")
268
265
 
269
- if self.path.startswith("/callback"):
270
- # Return a page that extracts token from URL hash fragment
271
- self.send_response(200)
272
- self.send_header("Content-type", "text/html")
273
- self.end_headers()
274
-
275
- html = """
276
- <html>
277
- <head><title>Authentication Successful</title></head>
278
- <body>
279
- <h1>Authentication Successful!</h1>
280
- <p>You can close this window and return to the application.</p>
281
- <script>
282
- // Extract token from URL hash
283
- const hash = window.location.hash.substring(1);
284
- const params = new URLSearchParams(hash);
285
- const token = params.get('access_token');
286
- const expires_in = params.get('expires_in');
287
-
288
- // Send token back to server using fetch
289
- fetch('/token?' + new URLSearchParams({
290
- token: token,
291
- expires_in: expires_in
292
- }))
293
- .then(response => console.log('Token sent to app'));
294
- </script>
295
- </body>
296
- </html>
297
- """
298
- self.wfile.write(html.encode())
299
- return
266
+ # Exchange the short-lived token for a long-lived token
267
+ short_lived_token = token_container['token']
268
+ long_lived_token_info = exchange_token_for_long_lived(short_lived_token)
300
269
 
301
- if self.path.startswith("/confirm-update"):
302
- # Parse query parameters
303
- query = parse_qs(urlparse(self.path).query)
304
- adset_id = query.get("adset_id", [""])[0]
305
- token = query.get("token", [""])[0]
306
- changes = query.get("changes", ["{}"])[0]
270
+ if long_lived_token_info:
271
+ logger.info(f"Successfully exchanged for long-lived token (expires in {long_lived_token_info.expires_in} seconds)")
307
272
 
308
273
  try:
309
- changes_dict = json.loads(changes)
310
- except json.JSONDecodeError:
311
- changes_dict = {}
312
-
313
- # Return confirmation page
314
- self.send_response(200)
315
- self.send_header("Content-type", "text/html")
316
- self.end_headers()
317
-
318
- html = """
319
- <html>
320
- <head>
321
- <title>Confirm Ad Set Update</title>
322
- <style>
323
- body { font-family: Arial, sans-serif; margin: 20px; max-width: 1000px; margin: 0 auto; }
324
- .warning { color: #d73a49; margin: 20px 0; padding: 15px; border-left: 4px solid #d73a49; background-color: #fff8f8; }
325
- .changes { background: #f6f8fa; padding: 15px; border-radius: 6px; }
326
- .buttons { margin-top: 20px; }
327
- button { padding: 10px 20px; margin-right: 10px; border-radius: 6px; cursor: pointer; }
328
- .approve { background: #2ea44f; color: white; border: none; }
329
- .cancel { background: #d73a49; color: white; border: none; }
330
- .diff-table { width: 100%; border-collapse: collapse; margin: 15px 0; }
331
- .diff-table td { padding: 8px; border: 1px solid #ddd; }
332
- .diff-table .header { background: #f1f8ff; font-weight: bold; }
333
- .status { padding: 15px; margin-top: 20px; border-radius: 6px; display: none; }
334
- .success { background-color: #e6ffed; border: 1px solid #2ea44f; color: #22863a; }
335
- .error { background-color: #ffeef0; border: 1px solid #d73a49; color: #d73a49; }
336
- pre { white-space: pre-wrap; word-break: break-all; }
337
- </style>
338
- </head>
339
- <body>
340
- <h1>Confirm Ad Set Update</h1>
341
- <p>You are about to update Ad Set: <strong>""" + adset_id + """</strong></p>
342
-
343
- <div class="warning">
344
- <strong>⚠️ Warning:</strong> These changes will be applied immediately upon approval.
345
- """ + ("<br><strong>🔴 Important:</strong> This update includes status changes that may affect billing." if "status" in changes_dict else "") + """
346
- </div>
347
-
348
- <h2>Proposed Changes:</h2>
349
- <div class="changes">
350
- <table class="diff-table">
351
- <tr class="header">
352
- <td>Setting</td>
353
- <td>New Value</td>
354
- <td>Description</td>
355
- </tr>
356
- """
357
-
358
- # Special handling for frequency_control_specs
359
- for k, v in changes_dict.items():
360
- description = ""
361
- if k == "frequency_control_specs" and isinstance(v, list) and len(v) > 0:
362
- spec = v[0]
363
- if all(key in spec for key in ["event", "interval_days", "max_frequency"]):
364
- description = f"Cap to {spec['max_frequency']} {spec['event'].lower()} per {spec['interval_days']} days"
365
-
366
- # Format the value for display
367
- display_value = json.dumps(v, indent=2) if isinstance(v, (dict, list)) else str(v)
368
-
369
- html += f"""
370
- <tr>
371
- <td>{k}</td>
372
- <td><pre>{display_value}</pre></td>
373
- <td>{description}</td>
374
- </tr>
375
- """
376
-
377
- html += """
378
- </table>
379
- </div>
380
-
381
- <div class="buttons">
382
- <button class="approve" onclick="approveChanges()">Approve Changes</button>
383
- <button class="cancel" onclick="cancelChanges()">Cancel</button>
384
- </div>
385
-
386
- <div id="status" class="status"></div>
387
-
388
- <script>
389
- function showStatus(message, isError = false) {
390
- const statusElement = document.getElementById('status');
391
- statusElement.textContent = message;
392
- statusElement.style.display = 'block';
393
- if (isError) {
394
- statusElement.classList.add('error');
395
- statusElement.classList.remove('success');
396
- } else {
397
- statusElement.classList.add('success');
398
- statusElement.classList.remove('error');
399
- }
400
- }
401
-
402
- function approveChanges() {
403
- showStatus("Processing update...");
404
-
405
- const buttons = document.querySelectorAll('button');
406
- buttons.forEach(button => button.disabled = true);
407
-
408
- fetch('/update-confirm?' + new URLSearchParams({
409
- adset_id: '""" + adset_id + """',
410
- token: '""" + token + """',
411
- changes: '""" + changes + """',
412
- action: 'approve'
413
- }))
414
- .then(response => response.json())
415
- .then(data => {
416
- if (data.error) {
417
- showStatus('Error applying changes: ' + data.error, true);
418
- buttons.forEach(button => button.disabled = false);
419
- } else {
420
- showStatus('Changes approved and will be applied shortly!');
421
- setTimeout(() => {
422
- window.location.href = '/verify-update?' + new URLSearchParams({
423
- adset_id: '""" + adset_id + """',
424
- token: '""" + token + """'
425
- });
426
- }, 3000);
427
- }
428
- })
429
- .catch(error => {
430
- showStatus('Error applying changes: ' + error, true);
431
- buttons.forEach(button => button.disabled = false);
432
- });
433
- }
434
-
435
- function cancelChanges() {
436
- showStatus("Cancelling update...");
437
-
438
- fetch('/update-confirm?' + new URLSearchParams({
439
- adset_id: '""" + adset_id + """',
440
- action: 'cancel'
441
- }))
442
- .then(() => {
443
- showStatus('Update cancelled.');
444
- setTimeout(() => window.close(), 2000);
445
- });
446
- }
447
- </script>
448
- </body>
449
- </html>
450
- """
451
- self.wfile.write(html.encode())
452
- return
453
-
454
- if self.path.startswith("/verify-update"):
455
- # Parse query parameters
456
- query = parse_qs(urlparse(self.path).query)
457
- adset_id = query.get("adset_id", [""])[0]
458
- token = query.get("token", [""])[0]
459
-
460
- # Respond with a verification page
461
- self.send_response(200)
462
- self.send_header("Content-type", "text/html")
463
- self.end_headers()
464
-
465
- html = """
466
- <html>
467
- <head>
468
- <title>Verifying Ad Set Update</title>
469
- <style>
470
- body { font-family: Arial, sans-serif; margin: 20px; max-width: 800px; margin: 0 auto; }
471
- .status { padding: 15px; margin-top: 20px; border-radius: 6px; }
472
- .loading { background-color: #f1f8ff; border: 1px solid #0366d6; }
473
- .success { background-color: #e6ffed; border: 1px solid #2ea44f; color: #22863a; }
474
- .error { background-color: #ffeef0; border: 1px solid #d73a49; color: #d73a49; }
475
- .details { background: #f6f8fa; padding: 15px; border-radius: 6px; margin-top: 20px; }
476
- .note { background-color: #fff8c5; border: 1px solid #e36209; padding: 15px; border-radius: 6px; margin: 20px 0; }
477
- pre { white-space: pre-wrap; word-break: break-all; }
478
- .spinner { display: inline-block; width: 20px; height: 20px; border: 3px solid rgba(0, 0, 0, 0.1); border-radius: 50%; border-top-color: #0366d6; animation: spin 1s ease-in-out infinite; margin-right: 10px; }
479
- @keyframes spin { to { transform: rotate(360deg); } }
480
- </style>
481
- </head>
482
- <body>
483
- <h1>Verifying Ad Set Update</h1>
484
- <p>Checking the status of your update for Ad Set <strong>""" + adset_id + """</strong></p>
485
-
486
- <div class="note">
487
- <strong>Note about Meta API Visibility:</strong>
488
- <p>Some fields (like frequency caps) may not be visible in the API response even when successfully set. This is a limitation of the Meta API and depends on factors like the ad set's optimization goal. You can verify these settings in the Meta Ads Manager UI or by monitoring metrics like frequency in the ad insights.</p>
489
- </div>
490
-
491
- <div id="status" class="status loading">
492
- <div class="spinner"></div> Verifying update...
493
- </div>
274
+ auth_manager.token_info = long_lived_token_info
275
+ logger.info(f"Long-lived token info set in auth_manager, expires in {long_lived_token_info.expires_in} seconds")
276
+ except NameError:
277
+ logger.error("auth_manager not defined when trying to process token")
494
278
 
495
- <div id="details" class="details" style="display: none;">
496
- <h3>Updated Ad Set Details:</h3>
497
- <pre id="adset-details">Loading...</pre>
498
- </div>
499
-
500
- <script>
501
- // Function to fetch the ad set details and check if frequency_control_specs was updated
502
- async function verifyUpdate() {
503
- try {
504
- const response = await fetch('/api/adset?' + new URLSearchParams({
505
- adset_id: '""" + adset_id + """',
506
- token: '""" + token + """'
507
- }));
508
-
509
- const data = await response.json();
510
- const statusElement = document.getElementById('status');
511
- const detailsElement = document.getElementById('details');
512
- const adsetDetailsElement = document.getElementById('adset-details');
513
-
514
- detailsElement.style.display = 'block';
515
- adsetDetailsElement.textContent = JSON.stringify(data, null, 2);
516
-
517
- // Update success message to reflect API visibility limitations
518
- statusElement.classList.remove('loading');
519
- statusElement.classList.add('success');
520
- statusElement.innerHTML = '✅ Update request was processed successfully. Please verify the changes in Meta Ads Manager UI or monitor ad performance metrics.';
521
- } catch (error) {
522
- const statusElement = document.getElementById('status');
523
- statusElement.classList.remove('loading');
524
- statusElement.classList.add('error');
525
- statusElement.textContent = 'Error verifying update: ' + error;
526
- }
527
- }
528
-
529
- // Wait a moment before verifying
530
- setTimeout(verifyUpdate, 3000);
531
- </script>
532
- </body>
533
- </html>
534
- """
535
- self.wfile.write(html.encode())
536
- return
537
-
538
- if self.path.startswith("/api/adset"):
539
- # Parse query parameters
540
- query = parse_qs(urlparse(self.path).query)
541
- adset_id = query.get("adset_id", [""])[0]
542
- token = query.get("token", [""])[0]
543
-
544
- from .api import make_api_request
545
-
546
- # Call the Graph API directly
547
- async def get_adset_data():
548
- endpoint = f"{adset_id}"
549
- params = {
550
- "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"
551
- }
552
-
553
- return await make_api_request(endpoint, token, params)
554
-
555
- # Run the async function
556
- loop = asyncio.new_event_loop()
557
- asyncio.set_event_loop(loop)
558
- result = loop.run_until_complete(get_adset_data())
559
- loop.close()
560
-
561
- # Return the result
562
- self.send_response(200)
563
- self.send_header("Content-type", "application/json")
564
- self.end_headers()
565
- self.wfile.write(json.dumps(result, indent=2).encode())
566
- return
567
-
568
- if self.path.startswith("/update-confirm"):
569
- # Handle update confirmation response
570
- query = parse_qs(urlparse(self.path).query)
571
- action = query.get("action", [""])[0]
572
-
573
- self.send_response(200)
574
- self.send_header("Content-type", "application/json")
575
- self.end_headers()
576
-
577
- if action == "approve":
578
- adset_id = query.get("adset_id", [""])[0]
579
- token = query.get("token", [""])[0]
580
- changes = query.get("changes", ["{}"])[0]
581
-
582
- # Store the approval in a global variable for the main thread to process
583
- global update_confirmation
584
- update_confirmation = {
585
- "approved": True,
586
- "adset_id": adset_id,
587
- "token": token,
588
- "changes": changes
589
- }
590
-
591
- # Prepare the API call to actually execute the update
592
- from .api import make_api_request
593
-
594
- # Function to perform the actual update
595
- async def perform_update():
596
- try:
597
- changes_dict = json.loads(changes)
598
- endpoint = f"{adset_id}"
599
-
600
- # Create a copy of changes_dict for the API call
601
- api_params = dict(changes_dict)
602
- api_params["access_token"] = token
603
-
604
- # Make the API request to update the ad set
605
- result = await make_api_request(endpoint, token, api_params, method="POST")
606
- return {"status": "approved", "api_result": result}
607
- except Exception as e:
608
- return {"status": "error", "error": str(e)}
279
+ try:
280
+ logger.info("Attempting to save long-lived token to cache")
281
+ auth_manager._save_token_to_cache()
282
+ logger.info(f"Long-lived token successfully saved to cache at {auth_manager._get_token_cache_path()}")
283
+ except Exception as e:
284
+ logger.error(f"Error saving token to cache: {e}")
609
285
 
610
- # Run the async function
611
- try:
612
- loop = asyncio.new_event_loop()
613
- asyncio.set_event_loop(loop)
614
- result = loop.run_until_complete(perform_update())
615
- loop.close()
616
-
617
- self.wfile.write(json.dumps(result).encode())
618
- except Exception as e:
619
- self.wfile.write(json.dumps({"status": "error", "error": str(e)}).encode())
620
- else:
621
- # Store the cancellation
622
- update_confirmation = {
623
- "approved": False
624
- }
625
- self.wfile.write(json.dumps({"status": "cancelled"}).encode())
626
- return
627
-
628
- if self.path.startswith("/token"):
629
- # Extract token from query params
630
- query = parse_qs(urlparse(self.path).query)
631
- token_container["token"] = query.get("token", [""])[0]
632
-
633
- if "expires_in" in query:
634
- try:
635
- token_container["expires_in"] = int(query.get("expires_in", ["0"])[0])
636
- except ValueError:
637
- token_container["expires_in"] = None
638
-
639
- # Send success response
640
- self.send_response(200)
641
- self.send_header("Content-type", "text/plain")
642
- self.end_headers()
643
- self.wfile.write(b"Token received")
286
+ needs_authentication = False
287
+ return True
288
+ else:
289
+ # Fall back to the short-lived token if exchange fails
290
+ logger.warning("Failed to exchange for long-lived token, using short-lived token instead")
291
+ token_info = TokenInfo(
292
+ access_token=short_lived_token,
293
+ expires_in=token_container.get('expires_in', 0)
294
+ )
644
295
 
645
- # Process the token (save it) immediately
646
- if token_container["token"]:
647
- # Create token info and save to cache
648
- logger.info("Token received in callback handler, attempting to save to cache")
649
- token_info = TokenInfo(
650
- access_token=token_container["token"],
651
- expires_in=token_container["expires_in"]
652
- )
653
-
654
- try:
655
- # Set the token info in the auth_manager first
656
- global auth_manager
657
- auth_manager.token_info = token_info
658
- logger.info(f"Token info set in auth_manager, expires in {token_info.expires_in} seconds")
659
-
660
- # Save to cache
661
- auth_manager._save_token_to_cache()
662
- logger.info(f"Token successfully saved to cache at {auth_manager._get_token_cache_path()}")
663
- except Exception as e:
664
- logger.error(f"Error saving token to cache: {e}")
296
+ try:
297
+ auth_manager.token_info = token_info
298
+ logger.info(f"Short-lived token info set in auth_manager, expires in {token_info.expires_in} seconds")
299
+ except NameError:
300
+ logger.error("auth_manager not defined when trying to process token")
665
301
 
666
- # Reset auth needed flag
667
- needs_authentication = False
302
+ try:
303
+ logger.info("Attempting to save token to cache")
304
+ auth_manager._save_token_to_cache()
305
+ logger.info(f"Token successfully saved to cache at {auth_manager._get_token_cache_path()}")
306
+ except Exception as e:
307
+ logger.error(f"Error saving token to cache: {e}")
668
308
 
669
- return token_container["token"]
670
- else:
671
- logger.warning("Received empty token in callback")
672
- needs_authentication = True
673
- return None
674
-
675
- # Silence server logs
676
- def log_message(self, format, *args):
677
- return
309
+ needs_authentication = False
310
+ return True
311
+ else:
312
+ logger.warning("Received empty token in process_token_response")
313
+ needs_authentication = True
314
+ return False
678
315
 
679
316
 
680
- def start_callback_server():
681
- """Start the callback server if it's not already running"""
682
- global callback_server_thread, callback_server_running, callback_server_port, auth_manager
317
+ def exchange_token_for_long_lived(short_lived_token):
318
+ """
319
+ Exchange a short-lived token for a long-lived token (60 days validity).
683
320
 
684
- with callback_server_lock:
685
- if callback_server_running:
686
- print(f"Callback server already running on port {callback_server_port}")
687
- return callback_server_port
321
+ Args:
322
+ short_lived_token: The short-lived access token received from OAuth flow
688
323
 
689
- # Find an available port
690
- port = 8888
691
- max_attempts = 10
692
- for attempt in range(max_attempts):
693
- try:
694
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
695
- s.bind(('localhost', port))
696
- break
697
- except OSError:
698
- port += 1
699
- if attempt == max_attempts - 1:
700
- raise Exception(f"Could not find an available port after {max_attempts} attempts")
324
+ Returns:
325
+ TokenInfo object with the long-lived token, or None if exchange failed
326
+ """
327
+ logger.info("Attempting to exchange short-lived token for long-lived token")
328
+
329
+ try:
330
+ # Get the app ID from the configuration
331
+ app_id = meta_config.get_app_id()
701
332
 
702
- # Update auth manager's redirect URI with new port
703
- if 'auth_manager' in globals():
704
- auth_manager.redirect_uri = f"http://localhost:{port}/callback"
705
- callback_server_port = port
333
+ # Get the app secret - this should be securely stored
334
+ app_secret = os.environ.get("META_APP_SECRET", "")
706
335
 
707
- try:
708
- # Get the CallbackHandler class from global scope
709
- handler_class = globals()['CallbackHandler']
710
-
711
- # Create and start server in a daemon thread
712
- server = HTTPServer(('localhost', port), handler_class)
713
- print(f"Callback server starting on port {port}")
714
-
715
- # Create a simple flag to signal when the server is ready
716
- server_ready = threading.Event()
717
-
718
- def server_thread():
719
- try:
720
- # Signal that the server thread has started
721
- server_ready.set()
722
- print(f"Callback server is now ready on port {port}")
723
- # Start serving HTTP requests
724
- server.serve_forever()
725
- except Exception as e:
726
- print(f"Server error: {e}")
727
- finally:
728
- with callback_server_lock:
729
- global callback_server_running
730
- callback_server_running = False
731
-
732
- callback_server_thread = threading.Thread(target=server_thread)
733
- callback_server_thread.daemon = True
734
- callback_server_thread.start()
735
-
736
- # Wait for server to be ready (up to 5 seconds)
737
- if not server_ready.wait(timeout=5):
738
- print("Warning: Timeout waiting for server to start, but continuing anyway")
739
-
740
- callback_server_running = True
741
-
742
- # Verify the server is actually accepting connections
743
- try:
744
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
745
- s.settimeout(2)
746
- s.connect(('localhost', port))
747
- print(f"Confirmed server is accepting connections on port {port}")
748
- except Exception as e:
749
- print(f"Warning: Could not verify server connection: {e}")
750
-
751
- return port
336
+ if not app_id or not app_secret:
337
+ logger.error("Missing app_id or app_secret for token exchange")
338
+ return None
752
339
 
753
- except Exception as e:
754
- print(f"Error starting callback server: {e}")
755
- # Try again with a different port in case of bind issues
756
- if "address already in use" in str(e).lower():
757
- print("Port may be in use, trying a different port...")
758
- return start_callback_server() # Recursive call with a new port
759
- raise e
760
-
761
-
762
- def process_token_response(token_container):
763
- """Process the token response from Facebook."""
764
- global needs_authentication
765
-
766
- if token_container and token_container.get('token'):
767
- logger.info("Processing token response from Facebook OAuth")
768
- token_info = TokenInfo(
769
- access_token=token_container['token'],
770
- expires_in=token_container.get('expires_in', 0)
771
- )
340
+ # Make the API request to exchange the token
341
+ url = "https://graph.facebook.com/v18.0/oauth/access_token"
342
+ params = {
343
+ "grant_type": "fb_exchange_token",
344
+ "client_id": app_id,
345
+ "client_secret": app_secret,
346
+ "fb_exchange_token": short_lived_token
347
+ }
772
348
 
773
- try:
774
- global auth_manager
775
- auth_manager.token_info = token_info
776
- logger.info(f"Token info set in auth_manager, expires in {token_info.expires_in} seconds")
777
- except NameError:
778
- logger.error("auth_manager not defined when trying to process token")
349
+ logger.debug(f"Making token exchange request to {url}")
350
+ response = requests.get(url, params=params)
351
+
352
+ if response.status_code == 200:
353
+ data = response.json()
354
+ logger.debug(f"Token exchange response: {data}")
779
355
 
780
- try:
781
- logger.info("Attempting to save token to cache")
782
- auth_manager._save_token_to_cache()
783
- logger.info(f"Token successfully saved to cache at {auth_manager._get_token_cache_path()}")
784
- except Exception as e:
785
- logger.error(f"Error saving token to cache: {e}")
356
+ # Create TokenInfo from the response
357
+ # The response includes access_token and expires_in (in seconds)
358
+ new_token = data.get("access_token")
359
+ expires_in = data.get("expires_in")
786
360
 
787
- needs_authentication = False
788
- return True
789
- else:
790
- logger.warning("Received empty token in process_token_response")
791
- needs_authentication = True
792
- return False
361
+ if new_token:
362
+ logger.info(f"Received long-lived token, expires in {expires_in} seconds (~{expires_in//86400} days)")
363
+ return TokenInfo(
364
+ access_token=new_token,
365
+ expires_in=expires_in
366
+ )
367
+ else:
368
+ logger.error("No access_token in exchange response")
369
+ return None
370
+ else:
371
+ logger.error(f"Token exchange failed with status {response.status_code}: {response.text}")
372
+ return None
373
+ except Exception as e:
374
+ logger.error(f"Error exchanging token: {e}")
375
+ return None
793
376
 
794
377
 
795
378
  async def get_current_access_token() -> Optional[str]: