meta-ads-mcp 0.2.5__py3-none-any.whl → 0.2.8__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.
@@ -0,0 +1,484 @@
1
+ """Authentication with Meta Ads API via pipeboard.co."""
2
+
3
+ import os
4
+ import json
5
+ import time
6
+ import requests
7
+ from pathlib import Path
8
+ import platform
9
+ from typing import Optional, Dict, Any
10
+ from .utils import logger
11
+
12
+ # Enable more detailed logging
13
+ import logging
14
+ logger.setLevel(logging.DEBUG)
15
+
16
+ # Base URL for pipeboard API
17
+ PIPEBOARD_API_BASE = "https://pipeboard.co/api"
18
+
19
+ # Debug message about API base URL
20
+ logger.info(f"Pipeboard API base URL: {PIPEBOARD_API_BASE}")
21
+
22
+ class TokenInfo:
23
+ """Stores token information including expiration"""
24
+ def __init__(self, access_token: str, expires_at: str = None, token_type: str = None):
25
+ self.access_token = access_token
26
+ self.expires_at = expires_at
27
+ self.token_type = token_type
28
+ self.created_at = int(time.time())
29
+ logger.debug(f"TokenInfo created. Expires at: {expires_at if expires_at else 'Not specified'}")
30
+
31
+ def is_expired(self) -> bool:
32
+ """Check if the token is expired"""
33
+ if not self.expires_at:
34
+ logger.debug("No expiration date set for token, assuming not expired")
35
+ return False # If no expiration is set, assume it's not expired
36
+
37
+ # Parse ISO 8601 date format to timestamp
38
+ try:
39
+ # Convert the expires_at string to a timestamp
40
+ # Format is like "2023-12-31T23:59:59.999Z" or "2023-12-31T23:59:59.999+00:00"
41
+ from datetime import datetime
42
+
43
+ # Remove the Z suffix if present and handle +00:00 format
44
+ expires_at_str = self.expires_at
45
+ if expires_at_str.endswith('Z'):
46
+ expires_at_str = expires_at_str[:-1] # Remove Z
47
+
48
+ # Handle microseconds if present
49
+ if '.' in expires_at_str:
50
+ datetime_format = "%Y-%m-%dT%H:%M:%S.%f"
51
+ else:
52
+ datetime_format = "%Y-%m-%dT%H:%M:%S"
53
+
54
+ # Handle timezone offset
55
+ timezone_offset = "+00:00"
56
+ if "+" in expires_at_str:
57
+ expires_at_str, timezone_offset = expires_at_str.split("+")
58
+ timezone_offset = "+" + timezone_offset
59
+
60
+ # Parse the datetime without timezone info
61
+ expires_datetime = datetime.strptime(expires_at_str, datetime_format)
62
+
63
+ # Convert to timestamp (assume UTC)
64
+ expires_timestamp = expires_datetime.timestamp()
65
+ current_time = time.time()
66
+
67
+ # Check if token is expired and log result
68
+ is_expired = current_time > expires_timestamp
69
+ time_diff = expires_timestamp - current_time
70
+ if is_expired:
71
+ logger.debug(f"Token is expired! Current time: {datetime.fromtimestamp(current_time)}, "
72
+ f"Expires at: {datetime.fromtimestamp(expires_timestamp)}, "
73
+ f"Expired {abs(time_diff):.0f} seconds ago")
74
+ else:
75
+ logger.debug(f"Token is still valid. Expires at: {datetime.fromtimestamp(expires_timestamp)}, "
76
+ f"Time remaining: {time_diff:.0f} seconds")
77
+
78
+ return is_expired
79
+ except Exception as e:
80
+ logger.error(f"Error parsing expiration date: {e}")
81
+ # Log the actual value to help diagnose format issues
82
+ logger.error(f"Invalid expires_at value: '{self.expires_at}'")
83
+ # Log detailed error information
84
+ import traceback
85
+ logger.error(f"Traceback: {traceback.format_exc()}")
86
+ return False # If we can't parse the date, assume it's not expired
87
+
88
+ def serialize(self) -> Dict[str, Any]:
89
+ """Convert to a dictionary for storage"""
90
+ return {
91
+ "access_token": self.access_token,
92
+ "expires_at": self.expires_at,
93
+ "token_type": self.token_type,
94
+ "created_at": self.created_at
95
+ }
96
+
97
+ @classmethod
98
+ def deserialize(cls, data: Dict[str, Any]) -> 'TokenInfo':
99
+ """Create from a stored dictionary"""
100
+ logger.debug(f"Deserializing token data with keys: {', '.join(data.keys())}")
101
+ if 'expires_at' in data:
102
+ logger.debug(f"Token expires_at from cache: {data['expires_at']}")
103
+
104
+ token = cls(
105
+ access_token=data.get("access_token", ""),
106
+ expires_at=data.get("expires_at"),
107
+ token_type=data.get("token_type")
108
+ )
109
+ token.created_at = data.get("created_at", int(time.time()))
110
+ return token
111
+
112
+
113
+ class PipeboardAuthManager:
114
+ """Manages authentication with Meta APIs via pipeboard.co"""
115
+ def __init__(self):
116
+ self.api_token = os.environ.get("PIPEBOARD_API_TOKEN", "")
117
+ logger.debug(f"PipeboardAuthManager initialized with API token: {self.api_token[:5]}..." if self.api_token else "No API token")
118
+ if self.api_token:
119
+ logger.info("Pipeboard authentication enabled. Will use pipeboard.co for Meta authentication.")
120
+ else:
121
+ logger.info("Pipeboard authentication not enabled. Set PIPEBOARD_API_TOKEN environment variable to enable.")
122
+ self.token_info = None
123
+ self._load_cached_token()
124
+
125
+ def _get_token_cache_path(self) -> Path:
126
+ """Get the platform-specific path for token cache file"""
127
+ if platform.system() == "Windows":
128
+ base_path = Path(os.environ.get("APPDATA", ""))
129
+ elif platform.system() == "Darwin": # macOS
130
+ base_path = Path.home() / "Library" / "Application Support"
131
+ else: # Assume Linux/Unix
132
+ base_path = Path.home() / ".config"
133
+
134
+ # Create directory if it doesn't exist
135
+ cache_dir = base_path / "meta-ads-mcp"
136
+ cache_dir.mkdir(parents=True, exist_ok=True)
137
+
138
+ cache_path = cache_dir / "pipeboard_token_cache.json"
139
+ logger.debug(f"Token cache path: {cache_path}")
140
+ return cache_path
141
+
142
+ def _load_cached_token(self) -> bool:
143
+ """Load token from cache if available"""
144
+ cache_path = self._get_token_cache_path()
145
+
146
+ if not cache_path.exists():
147
+ logger.debug(f"Token cache file not found at {cache_path}")
148
+ return False
149
+
150
+ try:
151
+ with open(cache_path, "r") as f:
152
+ logger.debug(f"Reading token cache from {cache_path}")
153
+ data = json.load(f)
154
+ self.token_info = TokenInfo.deserialize(data)
155
+
156
+ # Log token details (partial token for security)
157
+ masked_token = self.token_info.access_token[:10] + "..." + self.token_info.access_token[-5:] if self.token_info.access_token else "None"
158
+ logger.debug(f"Loaded token: {masked_token}")
159
+
160
+ # Check if token is expired
161
+ if self.token_info.is_expired():
162
+ logger.info("Cached token is expired")
163
+ self.token_info = None
164
+ return False
165
+
166
+ logger.info(f"Loaded cached token (expires at {self.token_info.expires_at})")
167
+ return True
168
+ except json.JSONDecodeError as e:
169
+ logger.error(f"Error parsing token cache file: {e}")
170
+ logger.debug("Token cache file might be corrupted, trying to read raw content")
171
+ try:
172
+ with open(cache_path, "r") as f:
173
+ raw_content = f.read()
174
+ logger.debug(f"Raw cache file content (first 100 chars): {raw_content[:100]}")
175
+ except Exception as e2:
176
+ logger.error(f"Could not read raw cache file: {e2}")
177
+ return False
178
+ except Exception as e:
179
+ logger.error(f"Error loading cached token: {e}")
180
+ return False
181
+
182
+ def _save_token_to_cache(self) -> None:
183
+ """Save token to cache file"""
184
+ if not self.token_info:
185
+ logger.debug("No token to save to cache")
186
+ return
187
+
188
+ cache_path = self._get_token_cache_path()
189
+
190
+ try:
191
+ token_data = self.token_info.serialize()
192
+ logger.debug(f"Saving token to cache. Expires at: {token_data.get('expires_at')}")
193
+
194
+ with open(cache_path, "w") as f:
195
+ json.dump(token_data, f)
196
+ logger.info(f"Token cached at: {cache_path}")
197
+ except Exception as e:
198
+ logger.error(f"Error saving token to cache: {e}")
199
+
200
+ def initiate_auth_flow(self) -> Dict[str, str]:
201
+ """
202
+ Initiate the Meta OAuth flow via pipeboard.co
203
+
204
+ Returns:
205
+ Dict with loginUrl and status info
206
+ """
207
+ if not self.api_token:
208
+ logger.error("No PIPEBOARD_API_TOKEN environment variable set")
209
+ raise ValueError("No PIPEBOARD_API_TOKEN environment variable set")
210
+
211
+ # Exactly match the format used in meta_auth_test.sh
212
+ url = f"{PIPEBOARD_API_BASE}/meta/auth?api_token={self.api_token}"
213
+ headers = {
214
+ "Content-Type": "application/json"
215
+ }
216
+
217
+ logger.info(f"Initiating auth flow with POST request to {url}")
218
+
219
+ try:
220
+ # Make the POST request exactly as in the working meta_auth_test.sh script
221
+ response = requests.post(url, headers=headers)
222
+ logger.info(f"Auth flow response status: {response.status_code}")
223
+
224
+ # Better error handling
225
+ if response.status_code != 200:
226
+ logger.error(f"Auth flow error: HTTP {response.status_code}")
227
+ error_text = response.text if response.text else "No response content"
228
+ logger.error(f"Response content: {error_text}")
229
+ if response.status_code == 404:
230
+ raise ValueError(f"Pipeboard API endpoint not found. Check if the server is running at {PIPEBOARD_API_BASE}")
231
+ elif response.status_code == 401:
232
+ raise ValueError(f"Unauthorized: Invalid API token. Check your PIPEBOARD_API_TOKEN.")
233
+
234
+ response.raise_for_status()
235
+
236
+ # Parse the response
237
+ try:
238
+ data = response.json()
239
+ logger.info(f"Received response keys: {', '.join(data.keys())}")
240
+ except json.JSONDecodeError:
241
+ logger.error(f"Could not parse JSON response: {response.text}")
242
+ raise ValueError(f"Invalid JSON response from auth endpoint: {response.text[:100]}")
243
+
244
+ # Log auth flow response (without sensitive information)
245
+ if 'loginUrl' in data:
246
+ logger.info(f"Auth flow initiated successfully with login URL: {data['loginUrl'][:30]}...")
247
+ else:
248
+ logger.warning(f"Auth flow response missing loginUrl field. Response keys: {', '.join(data.keys())}")
249
+
250
+ return data
251
+ except requests.exceptions.ConnectionError as e:
252
+ logger.error(f"Connection error to Pipeboard: {e}")
253
+ logger.debug(f"Attempting to connect to: {PIPEBOARD_API_BASE}")
254
+ raise
255
+ except requests.exceptions.Timeout as e:
256
+ logger.error(f"Timeout connecting to Pipeboard: {e}")
257
+ raise
258
+ except requests.exceptions.RequestException as e:
259
+ logger.error(f"Error initiating auth flow: {e}")
260
+ raise
261
+ except Exception as e:
262
+ logger.error(f"Unexpected error initiating auth flow: {e}")
263
+ raise
264
+
265
+ def get_access_token(self, force_refresh: bool = False) -> Optional[str]:
266
+ """
267
+ Get the current access token, refreshing if necessary or if forced
268
+
269
+ Args:
270
+ force_refresh: Force token refresh even if cached token exists
271
+
272
+ Returns:
273
+ Access token if available, None otherwise
274
+ """
275
+ # First check if API token is configured
276
+ if not self.api_token:
277
+ logger.error("TOKEN VALIDATION FAILED: No Pipeboard API token configured")
278
+ logger.error("Please set PIPEBOARD_API_TOKEN environment variable")
279
+ return None
280
+
281
+ # Check if we already have a valid token
282
+ if not force_refresh and self.token_info and not self.token_info.is_expired():
283
+ logger.debug("Using existing valid token")
284
+ return self.token_info.access_token
285
+
286
+ # If we have a token but it's expired, log that information
287
+ if not force_refresh and self.token_info and self.token_info.is_expired():
288
+ logger.error("TOKEN VALIDATION FAILED: Existing token is expired")
289
+ if self.token_info.expires_at:
290
+ logger.error(f"Token expiration time: {self.token_info.expires_at}")
291
+
292
+ logger.info(f"Getting new token (force_refresh={force_refresh})")
293
+
294
+ # If force refresh or no token/expired token, get a new one from Pipeboard
295
+ try:
296
+ # Make a request to get the token, using the same URL format as initiate_auth_flow
297
+ url = f"{PIPEBOARD_API_BASE}/meta/token?api_token={self.api_token}"
298
+ headers = {
299
+ "Content-Type": "application/json"
300
+ }
301
+
302
+ logger.info(f"Requesting token from {url}")
303
+
304
+ # Add timeout for better error messages
305
+ try:
306
+ response = requests.get(url, headers=headers, timeout=10)
307
+ except requests.exceptions.Timeout:
308
+ logger.error("TOKEN VALIDATION FAILED: Timeout while connecting to Pipeboard API")
309
+ logger.error(f"Could not connect to {PIPEBOARD_API_BASE} within 10 seconds")
310
+ return None
311
+ except requests.exceptions.ConnectionError:
312
+ logger.error("TOKEN VALIDATION FAILED: Connection error with Pipeboard API")
313
+ logger.error(f"Could not connect to {PIPEBOARD_API_BASE} - check if service is running")
314
+ return None
315
+
316
+ logger.info(f"Token request response status: {response.status_code}")
317
+
318
+ # Better error handling with response content
319
+ if response.status_code != 200:
320
+ logger.error(f"TOKEN VALIDATION FAILED: HTTP error {response.status_code}")
321
+ error_text = response.text if response.text else "No response content"
322
+ logger.error(f"Response content: {error_text}")
323
+
324
+ # Add more specific error messages for common status codes
325
+ if response.status_code == 401:
326
+ logger.error("Authentication failed: Invalid Pipeboard API token")
327
+ elif response.status_code == 404:
328
+ logger.error("Endpoint not found: Check if Pipeboard API service is running correctly")
329
+ elif response.status_code == 400:
330
+ logger.error("Bad request: The request to Pipeboard API was malformed")
331
+
332
+ response.raise_for_status()
333
+
334
+ try:
335
+ data = response.json()
336
+ logger.info(f"Received token response with keys: {', '.join(data.keys())}")
337
+ except json.JSONDecodeError:
338
+ logger.error("TOKEN VALIDATION FAILED: Invalid JSON response from Pipeboard API")
339
+ logger.error(f"Response content (first 100 chars): {response.text[:100]}")
340
+ return None
341
+
342
+ # Validate response data
343
+ if "access_token" not in data:
344
+ logger.error("TOKEN VALIDATION FAILED: No access_token in Pipeboard API response")
345
+ logger.error(f"Response keys: {', '.join(data.keys())}")
346
+ if "error" in data:
347
+ logger.error(f"Error details: {data['error']}")
348
+ else:
349
+ logger.error("No error information available in response")
350
+ return None
351
+
352
+ # Create new token info
353
+ self.token_info = TokenInfo(
354
+ access_token=data.get("access_token"),
355
+ expires_at=data.get("expires_at"),
356
+ token_type=data.get("token_type", "bearer")
357
+ )
358
+
359
+ # Save to cache
360
+ self._save_token_to_cache()
361
+
362
+ masked_token = self.token_info.access_token[:10] + "..." + self.token_info.access_token[-5:] if self.token_info.access_token else "None"
363
+ logger.info(f"Successfully retrieved access token: {masked_token}")
364
+ return self.token_info.access_token
365
+ except requests.RequestException as e:
366
+ status_code = e.response.status_code if hasattr(e, 'response') and e.response else None
367
+ response_text = e.response.text if hasattr(e, 'response') and e.response else "No response"
368
+
369
+ if status_code == 401:
370
+ logger.error(f"Unauthorized: Check your PIPEBOARD_API_TOKEN. Response: {response_text}")
371
+ elif status_code == 404:
372
+ logger.error(f"No token available: You might need to complete authorization first. Response: {response_text}")
373
+ # Return None so caller can handle the auth flow
374
+ return None
375
+ else:
376
+ logger.error(f"Error getting access token (status {status_code}): {e}")
377
+ logger.error(f"Response content: {response_text}")
378
+ return None
379
+ except Exception as e:
380
+ logger.error(f"Unexpected error getting access token: {e}")
381
+ return None
382
+
383
+ def invalidate_token(self) -> None:
384
+ """Invalidate the current token, usually because it has expired or is invalid"""
385
+ if self.token_info:
386
+ logger.info(f"Invalidating token: {self.token_info.access_token[:10]}...")
387
+ self.token_info = None
388
+
389
+ # Remove the cached token file
390
+ try:
391
+ cache_path = self._get_token_cache_path()
392
+ if cache_path.exists():
393
+ os.remove(cache_path)
394
+ logger.info(f"Removed cached token file: {cache_path}")
395
+ else:
396
+ logger.debug(f"No token cache file to remove: {cache_path}")
397
+ except Exception as e:
398
+ logger.error(f"Error removing cached token file: {e}")
399
+ else:
400
+ logger.debug("No token to invalidate")
401
+
402
+ def test_token_validity(self) -> bool:
403
+ """
404
+ Test if the current token is valid with the Meta Graph API
405
+
406
+ Returns:
407
+ True if valid, False otherwise
408
+ """
409
+ if not self.token_info or not self.token_info.access_token:
410
+ logger.debug("No token to test")
411
+ logger.error("TOKEN VALIDATION FAILED: Missing token to test")
412
+ return False
413
+
414
+ # Log token details for debugging (partial token for security)
415
+ masked_token = self.token_info.access_token[:5] + "..." + self.token_info.access_token[-5:] if self.token_info.access_token else "None"
416
+ token_type = self.token_info.token_type if hasattr(self.token_info, 'token_type') and self.token_info.token_type else "bearer"
417
+ logger.debug(f"Testing token validity (token: {masked_token}, type: {token_type})")
418
+
419
+ try:
420
+ # Make a simple request to the /me endpoint to test the token
421
+ META_GRAPH_API_VERSION = "v20.0"
422
+ url = f"https://graph.facebook.com/{META_GRAPH_API_VERSION}/me"
423
+ headers = {"Authorization": f"Bearer {self.token_info.access_token}"}
424
+
425
+ logger.debug(f"Testing token validity with request to {url}")
426
+
427
+ # Add timeout and better error handling
428
+ try:
429
+ response = requests.get(url, headers=headers, timeout=10)
430
+ except requests.exceptions.Timeout:
431
+ logger.error("TOKEN VALIDATION FAILED: Timeout while connecting to Meta API")
432
+ logger.error("The Graph API did not respond within 10 seconds")
433
+ return False
434
+ except requests.exceptions.ConnectionError:
435
+ logger.error("TOKEN VALIDATION FAILED: Connection error with Meta API")
436
+ logger.error("Could not establish connection to Graph API - check network connectivity")
437
+ return False
438
+
439
+ if response.status_code == 200:
440
+ data = response.json()
441
+ logger.debug(f"Token is valid. User ID: {data.get('id')}")
442
+ # Add more useful user information for debugging
443
+ user_info = f"User ID: {data.get('id')}"
444
+ if 'name' in data:
445
+ user_info += f", Name: {data.get('name')}"
446
+ logger.info(f"Meta API token validated successfully ({user_info})")
447
+ return True
448
+ else:
449
+ logger.error(f"TOKEN VALIDATION FAILED: API returned status {response.status_code}")
450
+
451
+ # Try to parse the error response for more detailed information
452
+ try:
453
+ error_data = response.json()
454
+ if 'error' in error_data:
455
+ error_obj = error_data.get('error', {})
456
+ error_code = error_obj.get('code', 'unknown')
457
+ error_message = error_obj.get('message', 'Unknown error')
458
+ logger.error(f"Meta API error: Code {error_code} - {error_message}")
459
+
460
+ # Add specific guidance for common error codes
461
+ if error_code == 190:
462
+ logger.error("Error indicates the token is invalid or has expired")
463
+ elif error_code == 4:
464
+ logger.error("Error indicates rate limiting - too many requests")
465
+ elif error_code == 200:
466
+ logger.error("Error indicates API permissions or configuration issue")
467
+ else:
468
+ logger.error(f"No error object in response: {error_data}")
469
+ except json.JSONDecodeError:
470
+ logger.error(f"Could not parse error response: {response.text[:200]}")
471
+
472
+ return False
473
+ except Exception as e:
474
+ logger.error(f"TOKEN VALIDATION FAILED: Unexpected error: {str(e)}")
475
+
476
+ # Add stack trace for debugging complex issues
477
+ import traceback
478
+ logger.error(f"Stack trace: {traceback.format_exc()}")
479
+
480
+ return False
481
+
482
+
483
+ # Create singleton instance
484
+ pipeboard_auth_manager = PipeboardAuthManager()
@@ -4,9 +4,12 @@ from mcp.server.fastmcp import FastMCP
4
4
  import argparse
5
5
  import os
6
6
  import sys
7
+ import webbrowser
7
8
  from .auth import login as login_auth
8
9
  from .resources import list_resources, get_resource
9
10
  from .utils import logger
11
+ from .pipeboard_auth import pipeboard_auth_manager
12
+ import time
10
13
 
11
14
  # Initialize FastMCP server
12
15
  mcp_server = FastMCP("meta-ads", use_consistent_tool_format=True)
@@ -78,7 +81,49 @@ def main():
78
81
  # Handle login command
79
82
  if args.login:
80
83
  login_cli()
81
- else:
82
- # Initialize and run the server
83
- logger.info("Starting MCP server with stdio transport")
84
- mcp_server.run(transport='stdio')
84
+ return 0
85
+
86
+ # Check for Pipeboard authentication and token
87
+ pipeboard_api_token = os.environ.get("PIPEBOARD_API_TOKEN")
88
+ if pipeboard_api_token:
89
+ logger.info("Using Pipeboard authentication")
90
+ # Check for existing token
91
+ token = pipeboard_auth_manager.get_access_token()
92
+ if not token:
93
+ logger.info("No valid Pipeboard token found. Initiating browser-based authentication flow.")
94
+ print("No valid Meta token found. Opening browser for authentication...")
95
+ try:
96
+ # Initialize the auth flow and get the login URL
97
+ auth_data = pipeboard_auth_manager.initiate_auth_flow()
98
+ login_url = auth_data.get('loginUrl')
99
+ if login_url:
100
+ logger.info(f"Opening browser with login URL: {login_url}")
101
+ webbrowser.open(login_url)
102
+ print("Please authorize the application in your browser.")
103
+ print("After authorization, the token will be automatically retrieved.")
104
+ print("Waiting for authentication to complete...")
105
+
106
+ # Poll for token completion
107
+ max_attempts = 30 # Try for 30 * 2 = 60 seconds
108
+ for attempt in range(max_attempts):
109
+ print(f"Waiting for authentication... ({attempt+1}/{max_attempts})")
110
+ # Try to get the token again
111
+ token = pipeboard_auth_manager.get_access_token(force_refresh=True)
112
+ if token:
113
+ print("Authentication successful!")
114
+ break
115
+ time.sleep(2) # Wait 2 seconds between attempts
116
+
117
+ if not token:
118
+ print("Authentication timed out. Starting server anyway.")
119
+ print("You may need to restart the server after completing authentication.")
120
+ else:
121
+ logger.error("No login URL received from Pipeboard API")
122
+ print("Error: Could not get authentication URL. Check your API token.")
123
+ except Exception as e:
124
+ logger.error(f"Error initiating browser-based authentication: {e}")
125
+ print(f"Error: Could not start authentication: {e}")
126
+
127
+ # Initialize and run the server
128
+ logger.info("Starting MCP server with stdio transport")
129
+ mcp_server.run(transport='stdio')
@@ -17,11 +17,16 @@ import platform
17
17
  META_APP_ID = os.environ.get("META_APP_ID", "")
18
18
  META_APP_SECRET = os.environ.get("META_APP_SECRET", "")
19
19
 
20
- # Print warning if Meta app credentials are not configured
21
- if not META_APP_ID:
22
- print("WARNING: META_APP_ID environment variable is not set. Authentication will not work properly.")
23
- if not META_APP_SECRET:
24
- print("WARNING: META_APP_SECRET environment variable is not set. Long-lived token exchange will not work.")
20
+ # Only show warnings about Meta credentials if we're not using Pipeboard
21
+ # Check for Pipeboard token in environment
22
+ using_pipeboard = bool(os.environ.get("PIPEBOARD_API_TOKEN", ""))
23
+
24
+ # Print warning if Meta app credentials are not configured and not using Pipeboard
25
+ if not using_pipeboard:
26
+ if not META_APP_ID:
27
+ print("WARNING: META_APP_ID environment variable is not set. Authentication will not work properly.")
28
+ if not META_APP_SECRET:
29
+ print("WARNING: META_APP_SECRET environment variable is not set. Long-lived token exchange will not work.")
25
30
 
26
31
  # Configure logging to file
27
32
  def setup_logging():
@@ -55,6 +60,7 @@ def setup_logging():
55
60
  # Log startup information
56
61
  logger.info(f"Logging initialized. Log file: {log_file}")
57
62
  logger.info(f"Platform: {platform.system()} {platform.release()}")
63
+ logger.info(f"Using Pipeboard authentication: {using_pipeboard}")
58
64
 
59
65
  return logger
60
66