meta-ads-mcp 0.2.6__py3-none-any.whl → 0.2.9__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.
@@ -32,15 +32,23 @@ async def get_adsets(access_token: str = None, account_id: str = None, limit: in
32
32
  else:
33
33
  return json.dumps({"error": "No account ID specified and no accounts found for user"}, indent=2)
34
34
 
35
- endpoint = f"{account_id}/adsets"
36
- params = {
37
- "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,frequency_control_specs{event,interval_days,max_frequency}",
38
- "limit": limit
39
- }
40
-
35
+ # Change endpoint based on whether campaign_id is provided
41
36
  if campaign_id:
42
- params["campaign_id"] = campaign_id
43
-
37
+ endpoint = f"{campaign_id}/adsets"
38
+ params = {
39
+ "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,frequency_control_specs{event,interval_days,max_frequency}",
40
+ "limit": limit
41
+ }
42
+ else:
43
+ # Use account endpoint if no campaign_id is given
44
+ endpoint = f"{account_id}/adsets"
45
+ params = {
46
+ "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,frequency_control_specs{event,interval_days,max_frequency}",
47
+ "limit": limit
48
+ }
49
+ # Note: Removed the attempt to add campaign_id to params for the account endpoint case,
50
+ # as it was ineffective and the logic now uses the correct endpoint for campaign filtering.
51
+
44
52
  data = await make_api_request(endpoint, access_token, params)
45
53
 
46
54
  return json.dumps(data, indent=2)
@@ -66,19 +74,133 @@ async def get_adset_details(access_token: str = None, adset_id: str = None) -> s
66
74
  return json.dumps({"error": "No ad set ID provided"}, indent=2)
67
75
 
68
76
  endpoint = f"{adset_id}"
77
+ # Explicitly prioritize frequency_control_specs in the fields request
69
78
  params = {
70
- "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{event,interval_days,max_frequency}"
79
+ "fields": "id,name,campaign_id,status,frequency_control_specs{event,interval_days,max_frequency},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"
71
80
  }
72
81
 
73
82
  data = await make_api_request(endpoint, access_token, params)
74
83
 
84
+ # For debugging - check if frequency_control_specs was returned
85
+ if 'frequency_control_specs' not in data:
86
+ data['_meta'] = {
87
+ 'note': 'No frequency_control_specs field was returned by the API. This means either no frequency caps are set or the API did not include this field in the response.'
88
+ }
89
+
75
90
  return json.dumps(data, indent=2)
76
91
 
77
92
 
93
+ @mcp_server.tool()
94
+ @meta_api_tool
95
+ async def create_adset(
96
+ account_id: str = None,
97
+ campaign_id: str = None,
98
+ name: str = None,
99
+ status: str = "PAUSED",
100
+ daily_budget = None,
101
+ lifetime_budget = None,
102
+ targeting: Dict[str, Any] = None,
103
+ optimization_goal: str = None,
104
+ billing_event: str = None,
105
+ bid_amount = None,
106
+ bid_strategy: str = None,
107
+ start_time: str = None,
108
+ end_time: str = None,
109
+ access_token: str = None
110
+ ) -> str:
111
+ """
112
+ Create a new ad set in a Meta Ads account.
113
+
114
+ Args:
115
+ account_id: Meta Ads account ID (format: act_XXXXXXXXX)
116
+ campaign_id: Meta Ads campaign ID this ad set belongs to
117
+ name: Ad set name
118
+ status: Initial ad set status (default: PAUSED)
119
+ daily_budget: Daily budget in account currency (in cents) as a string
120
+ lifetime_budget: Lifetime budget in account currency (in cents) as a string
121
+ targeting: Targeting specifications including age, location, interests, etc.
122
+ Use targeting_automation.advantage_audience=1 for automatic audience finding
123
+ optimization_goal: Conversion optimization goal (e.g., 'LINK_CLICKS', 'REACH', 'CONVERSIONS')
124
+ billing_event: How you're charged (e.g., 'IMPRESSIONS', 'LINK_CLICKS')
125
+ bid_amount: Bid amount in account currency (in cents)
126
+ bid_strategy: Bid strategy (e.g., 'LOWEST_COST', 'LOWEST_COST_WITH_BID_CAP')
127
+ start_time: Start time in ISO 8601 format (e.g., '2023-12-01T12:00:00-0800')
128
+ end_time: End time in ISO 8601 format
129
+ access_token: Meta API access token (optional - will use cached token if not provided)
130
+ """
131
+ # Check required parameters
132
+ if not account_id:
133
+ return json.dumps({"error": "No account ID provided"}, indent=2)
134
+
135
+ if not campaign_id:
136
+ return json.dumps({"error": "No campaign ID provided"}, indent=2)
137
+
138
+ if not name:
139
+ return json.dumps({"error": "No ad set name provided"}, indent=2)
140
+
141
+ if not optimization_goal:
142
+ return json.dumps({"error": "No optimization goal provided"}, indent=2)
143
+
144
+ if not billing_event:
145
+ return json.dumps({"error": "No billing event provided"}, indent=2)
146
+
147
+ # Basic targeting is required if not provided
148
+ if not targeting:
149
+ targeting = {
150
+ "age_min": 18,
151
+ "age_max": 65,
152
+ "geo_locations": {"countries": ["US"]},
153
+ "targeting_automation": {"advantage_audience": 1}
154
+ }
155
+
156
+ endpoint = f"{account_id}/adsets"
157
+
158
+ params = {
159
+ "name": name,
160
+ "campaign_id": campaign_id,
161
+ "status": status,
162
+ "optimization_goal": optimization_goal,
163
+ "billing_event": billing_event,
164
+ "targeting": json.dumps(targeting) # Properly format as JSON string
165
+ }
166
+
167
+ # Convert budget values to strings if they aren't already
168
+ if daily_budget is not None:
169
+ params["daily_budget"] = str(daily_budget)
170
+
171
+ if lifetime_budget is not None:
172
+ params["lifetime_budget"] = str(lifetime_budget)
173
+
174
+ # Add other parameters if provided
175
+ if bid_amount is not None:
176
+ params["bid_amount"] = str(bid_amount)
177
+
178
+ if bid_strategy:
179
+ params["bid_strategy"] = bid_strategy
180
+
181
+ if start_time:
182
+ params["start_time"] = start_time
183
+
184
+ if end_time:
185
+ params["end_time"] = end_time
186
+
187
+ try:
188
+ data = await make_api_request(endpoint, access_token, params, method="POST")
189
+ return json.dumps(data, indent=2)
190
+ except Exception as e:
191
+ error_msg = str(e)
192
+ return json.dumps({
193
+ "error": "Failed to create ad set",
194
+ "details": error_msg,
195
+ "params_sent": params
196
+ }, indent=2)
197
+
198
+
78
199
  @mcp_server.tool()
79
200
  @meta_api_tool
80
201
  async def update_adset(adset_id: str, frequency_control_specs: List[Dict[str, Any]] = None, bid_strategy: str = None,
81
- bid_amount: int = None, status: str = None, targeting: Dict[str, Any] = None, access_token: str = None) -> str:
202
+ bid_amount: int = None, status: str = None, targeting: Dict[str, Any] = None,
203
+ optimization_goal: str = None, access_token: str = None) -> str:
82
204
  """
83
205
  Update an ad set with new settings including frequency caps.
84
206
 
@@ -91,6 +213,7 @@ async def update_adset(adset_id: str, frequency_control_specs: List[Dict[str, An
91
213
  status: Update ad set status (ACTIVE, PAUSED, etc.)
92
214
  targeting: Targeting specifications including targeting_automation
93
215
  (e.g. {"targeting_automation":{"advantage_audience":1}})
216
+ optimization_goal: Conversion optimization goal (e.g., 'LINK_CLICKS', 'CONVERSIONS', 'APP_INSTALLS', etc.)
94
217
  access_token: Meta API access token (optional - will use cached token if not provided)
95
218
  """
96
219
  if not adset_id:
@@ -110,6 +233,9 @@ async def update_adset(adset_id: str, frequency_control_specs: List[Dict[str, An
110
233
  if status is not None:
111
234
  changes['status'] = status
112
235
 
236
+ if optimization_goal is not None:
237
+ changes['optimization_goal'] = optimization_goal
238
+
113
239
  if targeting is not None:
114
240
  # Get current ad set details to preserve existing targeting settings
115
241
  current_details_json = await get_adset_details(adset_id=adset_id, access_token=access_token)
@@ -163,7 +289,7 @@ async def update_adset(adset_id: str, frequency_control_specs: List[Dict[str, An
163
289
  "current_details": current_details,
164
290
  "proposed_changes": changes,
165
291
  "instructions_for_llm": "You must present this link as clickable Markdown to the user using the markdown_link format provided.",
166
- "note": "After authenticating, the token will be automatically saved and your ad set will be updated. Refresh the browser page if it doesn't load immediately."
292
+ "note": "Click the link to confirm and apply your ad set updates. Refresh the browser page if it doesn't load immediately."
167
293
  }
168
294
 
169
295
  return json.dumps(response, indent=2)
meta_ads_mcp/core/api.py CHANGED
@@ -193,7 +193,7 @@ def meta_api_tool(func):
193
193
  logger.debug(f"Current app_id: {app_id}")
194
194
  logger.debug(f"META_APP_ID env var: {os.environ.get('META_APP_ID')}")
195
195
 
196
- # If access_token is not in kwargs, try to get it from auth_manager
196
+ # If access_token is not in kwargs or not kwargs['access_token'], try to get it from auth_manager
197
197
  if 'access_token' not in kwargs or not kwargs['access_token']:
198
198
  try:
199
199
  access_token = await get_current_access_token()
@@ -202,13 +202,36 @@ def meta_api_tool(func):
202
202
  logger.debug("Using access token from auth_manager")
203
203
  else:
204
204
  logger.warning("No access token available from auth_manager")
205
+ # Add more details about why token might be missing
206
+ if (auth_manager.app_id == "YOUR_META_APP_ID" or not auth_manager.app_id) and not auth_manager.use_pipeboard:
207
+ logger.error("TOKEN VALIDATION FAILED: No valid app_id configured")
208
+ logger.error("Please set META_APP_ID environment variable or configure in your code")
209
+ else:
210
+ logger.error("Check logs above for detailed token validation failures")
205
211
  except Exception as e:
206
212
  logger.error(f"Error getting access token: {str(e)}")
213
+ # Add stack trace for better debugging
214
+ import traceback
215
+ logger.error(f"Stack trace: {traceback.format_exc()}")
207
216
 
208
217
  # Final validation - if we still don't have a valid token, return authentication required
209
218
  if 'access_token' not in kwargs or not kwargs['access_token']:
210
219
  logger.warning("No access token available, authentication needed")
220
+
221
+ # Add more specific troubleshooting information
211
222
  auth_url = auth_manager.get_auth_url()
223
+ app_id = auth_manager.app_id
224
+
225
+ logger.error("TOKEN VALIDATION SUMMARY:")
226
+ logger.error(f"- Current app_id: '{app_id}'")
227
+ logger.error(f"- Environment META_APP_ID: '{os.environ.get('META_APP_ID', 'Not set')}'")
228
+ logger.error(f"- Pipeboard API token configured: {'Yes' if os.environ.get('PIPEBOARD_API_TOKEN') else 'No'}")
229
+
230
+ # Check for common configuration issues
231
+ if app_id == "YOUR_META_APP_ID" or not app_id:
232
+ logger.error("ISSUE DETECTED: No valid Meta App ID configured")
233
+ logger.error("ACTION REQUIRED: Set META_APP_ID environment variable with a valid App ID")
234
+
212
235
  return json.dumps({
213
236
  "error": {
214
237
  "message": "Authentication Required",
@@ -216,6 +239,11 @@ def meta_api_tool(func):
216
239
  "description": "You need to authenticate with the Meta API before using this tool",
217
240
  "action_required": "Please authenticate first",
218
241
  "auth_url": auth_url,
242
+ "configuration_status": {
243
+ "app_id_configured": bool(app_id) and app_id != "YOUR_META_APP_ID",
244
+ "pipeboard_enabled": bool(os.environ.get('PIPEBOARD_API_TOKEN')),
245
+ },
246
+ "troubleshooting": "Check logs for TOKEN VALIDATION FAILED messages",
219
247
  "markdown_link": f"[Click here to authenticate with Meta Ads API]({auth_url})"
220
248
  }
221
249
  }
meta_ads_mcp/core/auth.py CHANGED
@@ -18,8 +18,11 @@ from .callback_server import (
18
18
  update_confirmation
19
19
  )
20
20
 
21
+ # Import the new Pipeboard authentication
22
+ from .pipeboard_auth import pipeboard_auth_manager
23
+
21
24
  # Auth constants
22
- AUTH_SCOPE = "ads_management,ads_read,business_management"
25
+ AUTH_SCOPE = "ads_management,ads_read,business_management,public_profile"
23
26
  AUTH_REDIRECT_URI = "http://localhost:8888/callback"
24
27
  AUTH_RESPONSE_TYPE = "token"
25
28
 
@@ -39,8 +42,8 @@ class MetaConfig:
39
42
  if cls._instance is None:
40
43
  logger.debug("Creating new MetaConfig instance")
41
44
  cls._instance = super(MetaConfig, cls).__new__(cls)
42
- cls._instance.app_id = os.environ.get("META_APP_ID", "")
43
- logger.info(f"MetaConfig initialized with app_id from env: {cls._instance.app_id}")
45
+ cls._instance.app_id = os.environ.get("META_APP_ID", "779761636818489")
46
+ logger.info(f"MetaConfig initialized with app_id from env/default: {cls._instance.app_id}")
44
47
  return cls._instance
45
48
 
46
49
  def set_app_id(self, app_id):
@@ -123,7 +126,10 @@ class AuthManager:
123
126
  self.app_id = app_id
124
127
  self.redirect_uri = redirect_uri
125
128
  self.token_info = None
126
- self._load_cached_token()
129
+ # Check for Pipeboard token first
130
+ self.use_pipeboard = bool(os.environ.get("PIPEBOARD_API_TOKEN", ""))
131
+ if not self.use_pipeboard:
132
+ self._load_cached_token()
127
133
 
128
134
  def _get_token_cache_path(self) -> pathlib.Path:
129
135
  """Get the platform-specific path for token cache file"""
@@ -154,14 +160,14 @@ class AuthManager:
154
160
 
155
161
  # Check if token is expired
156
162
  if self.token_info.is_expired():
157
- print("Cached token is expired")
163
+ logger.info("Cached token is expired")
158
164
  self.token_info = None
159
165
  return False
160
166
 
161
- print(f"Loaded cached token (expires in {(self.token_info.created_at + self.token_info.expires_in) - int(time.time())} seconds)")
167
+ logger.info(f"Loaded cached token (expires in {(self.token_info.created_at + self.token_info.expires_in) - int(time.time())} seconds)")
162
168
  return True
163
169
  except Exception as e:
164
- print(f"Error loading cached token: {e}")
170
+ logger.error(f"Error loading cached token: {e}")
165
171
  return False
166
172
 
167
173
  def _save_token_to_cache(self) -> None:
@@ -174,9 +180,9 @@ class AuthManager:
174
180
  try:
175
181
  with open(cache_path, "w") as f:
176
182
  json.dump(self.token_info.serialize(), f)
177
- print(f"Token cached at: {cache_path}")
183
+ logger.info(f"Token cached at: {cache_path}")
178
184
  except Exception as e:
179
- print(f"Error saving token to cache: {e}")
185
+ logger.error(f"Error saving token to cache: {e}")
180
186
 
181
187
  def get_auth_url(self) -> str:
182
188
  """Generate the Facebook OAuth URL for desktop app flow"""
@@ -198,6 +204,12 @@ class AuthManager:
198
204
  Returns:
199
205
  Access token if successful, None otherwise
200
206
  """
207
+ # If Pipeboard auth is available, use that instead
208
+ if self.use_pipeboard:
209
+ logger.info("Using Pipeboard authentication")
210
+ return pipeboard_auth_manager.get_access_token(force_refresh=force_refresh)
211
+
212
+ # Otherwise, use the original OAuth flow
201
213
  # Check if we already have a valid token
202
214
  if not force_refresh and self.token_info and not self.token_info.is_expired():
203
215
  return self.token_info.access_token
@@ -212,7 +224,7 @@ class AuthManager:
212
224
  auth_url = self.get_auth_url()
213
225
 
214
226
  # Open browser with auth URL
215
- print(f"Opening browser with URL: {auth_url}")
227
+ logger.info(f"Opening browser with URL: {auth_url}")
216
228
  webbrowser.open(auth_url)
217
229
 
218
230
  # We don't wait for the token here anymore
@@ -227,6 +239,10 @@ class AuthManager:
227
239
  Returns:
228
240
  Access token if available, None otherwise
229
241
  """
242
+ # If using Pipeboard, always delegate to the Pipeboard auth manager
243
+ if self.use_pipeboard:
244
+ return pipeboard_auth_manager.get_access_token()
245
+
230
246
  if not self.token_info or self.token_info.is_expired():
231
247
  return None
232
248
 
@@ -234,8 +250,13 @@ class AuthManager:
234
250
 
235
251
  def invalidate_token(self) -> None:
236
252
  """Invalidate the current token, usually because it has expired or is invalid"""
253
+ # If using Pipeboard, delegate to the Pipeboard auth manager
254
+ if self.use_pipeboard:
255
+ pipeboard_auth_manager.invalidate_token()
256
+ return
257
+
237
258
  if self.token_info:
238
- print(f"Invalidating token: {self.token_info.access_token[:10]}...")
259
+ logger.info(f"Invalidating token: {self.token_info.access_token[:10]}...")
239
260
  self.token_info = None
240
261
 
241
262
  # Signal that authentication is needed
@@ -247,12 +268,12 @@ class AuthManager:
247
268
  cache_path = self._get_token_cache_path()
248
269
  if cache_path.exists():
249
270
  os.remove(cache_path)
250
- print(f"Removed cached token file: {cache_path}")
271
+ logger.info(f"Removed cached token file: {cache_path}")
251
272
  except Exception as e:
252
- print(f"Error removing cached token: {e}")
273
+ logger.error(f"Error removing cached token file: {e}")
253
274
 
254
275
  def clear_token(self) -> None:
255
- """Clear the current token and remove from cache"""
276
+ """Alias for invalidate_token for consistency with other APIs"""
256
277
  self.invalidate_token()
257
278
 
258
279
 
@@ -385,18 +406,55 @@ async def get_current_access_token() -> Optional[str]:
385
406
  app_id = meta_config.get_app_id()
386
407
  logger.debug(f"Current app_id: {app_id}")
387
408
 
409
+ # Check if using Pipeboard authentication
410
+ using_pipeboard = auth_manager.use_pipeboard
411
+
412
+ # Check if app_id is valid - but only if not using Pipeboard authentication
413
+ if not app_id and not using_pipeboard:
414
+ logger.error("TOKEN VALIDATION FAILED: No valid app_id configured")
415
+ logger.error("Please set META_APP_ID environment variable or configure via meta_config.set_app_id()")
416
+ return None
417
+
388
418
  # Attempt to get access token
389
419
  try:
390
420
  token = auth_manager.get_access_token()
391
421
 
392
422
  if token:
393
- logger.debug("Access token found in auth_manager")
423
+ # Add basic token validation - check if it looks like a valid token
424
+ if len(token) < 20: # Most Meta tokens are much longer
425
+ logger.error(f"TOKEN VALIDATION FAILED: Token appears malformed (length: {len(token)})")
426
+ auth_manager.invalidate_token()
427
+ return None
428
+
429
+ logger.debug(f"Access token found in auth_manager (starts with: {token[:10]}...)")
394
430
  return token
395
431
  else:
396
432
  logger.warning("No valid access token available in auth_manager")
433
+
434
+ # Check why token might be missing
435
+ if hasattr(auth_manager, 'token_info') and auth_manager.token_info:
436
+ if auth_manager.token_info.is_expired():
437
+ logger.error("TOKEN VALIDATION FAILED: Token is expired")
438
+ # Add expiration details
439
+ if hasattr(auth_manager.token_info, 'expires_in') and auth_manager.token_info.expires_in:
440
+ expiry_time = auth_manager.token_info.created_at + auth_manager.token_info.expires_in
441
+ current_time = int(time.time())
442
+ expired_seconds_ago = current_time - expiry_time
443
+ logger.error(f"Token expired {expired_seconds_ago} seconds ago")
444
+ elif not auth_manager.token_info.access_token:
445
+ logger.error("TOKEN VALIDATION FAILED: Token object exists but access_token is empty")
446
+ else:
447
+ logger.error("TOKEN VALIDATION FAILED: Token exists but was rejected for unknown reason")
448
+ else:
449
+ logger.error("TOKEN VALIDATION FAILED: No token information available")
450
+
451
+ # Suggest next steps for troubleshooting
452
+ logger.error("To fix: Try re-authenticating or check if your token has been revoked")
397
453
  return None
398
454
  except Exception as e:
399
455
  logger.error(f"Error getting access token: {str(e)}")
456
+ import traceback
457
+ logger.error(f"Token validation stacktrace: {traceback.format_exc()}")
400
458
  return None
401
459
 
402
460
 
@@ -443,4 +501,14 @@ def login():
443
501
 
444
502
  # Initialize auth manager with a placeholder - will be updated at runtime
445
503
  META_APP_ID = os.environ.get("META_APP_ID", "YOUR_META_APP_ID")
504
+
505
+ # Only show warnings about missing META_APP_ID/META_APP_SECRET when not using Pipeboard
506
+ if not os.environ.get("PIPEBOARD_API_TOKEN"):
507
+ # Log warnings about missing environment variables
508
+ if META_APP_ID == "YOUR_META_APP_ID":
509
+ logger.warning("META_APP_ID environment variable is not set. Authentication will not work properly.")
510
+
511
+ if not os.environ.get("META_APP_SECRET"):
512
+ logger.warning("META_APP_SECRET environment variable is not set. Long-lived token exchange will not work.")
513
+
446
514
  auth_manager = AuthManager(META_APP_ID)
@@ -2,10 +2,12 @@
2
2
 
3
3
  import json
4
4
  import asyncio
5
+ import os
5
6
  from .api import meta_api_tool
6
7
  from .auth import start_callback_server, auth_manager, get_current_access_token
7
8
  from .server import mcp_server
8
9
  from .utils import logger, META_APP_SECRET
10
+ from .pipeboard_auth import pipeboard_auth_manager
9
11
 
10
12
 
11
13
  @mcp_server.tool()
@@ -13,60 +15,112 @@ async def get_login_link(access_token: str = None) -> str:
13
15
  """
14
16
  Get a clickable login link for Meta Ads authentication.
15
17
 
18
+ NOTE: This method should only be used if you're using your own Facebook app.
19
+ If using Pipeboard authentication (recommended), set the PIPEBOARD_API_TOKEN
20
+ environment variable instead (token obtainable via https://pipeboard.co).
21
+
16
22
  Args:
17
23
  access_token: Meta API access token (optional - will use cached token if not provided)
18
24
 
19
25
  Returns:
20
26
  A clickable resource link for Meta authentication
21
27
  """
22
- # Check if we have a cached token
23
- cached_token = auth_manager.get_access_token()
24
- token_status = "No token" if not cached_token else "Valid token"
28
+ # Check if we're using pipeboard authentication
29
+ using_pipeboard = bool(os.environ.get("PIPEBOARD_API_TOKEN", ""))
25
30
 
26
- # If we already have a valid token and none was provided, just return success
27
- if cached_token and not access_token:
28
- logger.info("get_login_link called with existing valid token")
29
- return json.dumps({
30
- "message": "Already authenticated",
31
+ if using_pipeboard:
32
+ # Handle Pipeboard authentication
33
+ # Check if we have a cached token
34
+ cached_token = pipeboard_auth_manager.get_access_token()
35
+ token_status = "No token" if not cached_token else "Valid token"
36
+
37
+ # If we already have a valid token and none was provided, just return success
38
+ if cached_token and not access_token:
39
+ logger.info("get_login_link called with existing valid Pipeboard token")
40
+ return json.dumps({
41
+ "message": "Already authenticated with Pipeboard",
42
+ "token_status": token_status,
43
+ "token_preview": cached_token[:10] + "..." if cached_token else None,
44
+ "authentication_method": "pipeboard"
45
+ }, indent=2)
46
+
47
+ # Initiate the auth flow via Pipeboard
48
+ try:
49
+ auth_data = pipeboard_auth_manager.initiate_auth_flow()
50
+ login_url = auth_data.get("loginUrl")
51
+
52
+ # Return a special format that helps the LLM format the response properly
53
+ response = {
54
+ "login_url": login_url,
55
+ "token_status": token_status,
56
+ "markdown_link": f"[Click here to authenticate with Meta Ads via Pipeboard]({login_url})",
57
+ "message": "IMPORTANT: Please use the Markdown link format in your response to allow the user to click it.",
58
+ "instructions_for_llm": "You must present this link as clickable Markdown to the user using the markdown_link format provided.",
59
+ "authentication_method": "pipeboard",
60
+ "token_duration": "Approximately 60 days",
61
+ "note": "After authenticating, the token will be automatically saved."
62
+ }
63
+
64
+ return json.dumps(response, indent=2)
65
+ except Exception as e:
66
+ logger.error(f"Error initiating Pipeboard auth flow: {e}")
67
+ return json.dumps({
68
+ "error": f"Failed to initiate Pipeboard authentication: {str(e)}",
69
+ "message": "Please check your PIPEBOARD_API_TOKEN environment variable.",
70
+ "authentication_method": "pipeboard"
71
+ }, indent=2)
72
+ else:
73
+ # Original Meta authentication flow
74
+ # Check if we have a cached token
75
+ cached_token = auth_manager.get_access_token()
76
+ token_status = "No token" if not cached_token else "Valid token"
77
+
78
+ # If we already have a valid token and none was provided, just return success
79
+ if cached_token and not access_token:
80
+ logger.info("get_login_link called with existing valid token")
81
+ return json.dumps({
82
+ "message": "Already authenticated",
83
+ "token_status": token_status,
84
+ "token_preview": cached_token[:10] + "...",
85
+ "created_at": auth_manager.token_info.created_at if hasattr(auth_manager, "token_info") else None,
86
+ "expires_in": auth_manager.token_info.expires_in if hasattr(auth_manager, "token_info") else None,
87
+ "authentication_method": "meta_oauth"
88
+ }, indent=2)
89
+
90
+ # IMPORTANT: Start the callback server first by calling our helper function
91
+ # This ensures the server is ready before we provide the URL to the user
92
+ logger.info("Starting callback server for authentication")
93
+ port = start_callback_server()
94
+ logger.info(f"Callback server started on port {port}")
95
+
96
+ # Generate direct login URL
97
+ auth_manager.redirect_uri = f"http://localhost:{port}/callback" # Ensure port is set correctly
98
+ logger.info(f"Setting redirect URI to {auth_manager.redirect_uri}")
99
+ login_url = auth_manager.get_auth_url()
100
+ logger.info(f"Generated login URL: {login_url}")
101
+
102
+ # Check if we can exchange for long-lived tokens
103
+ token_exchange_supported = bool(META_APP_SECRET)
104
+ token_duration = "60 days" if token_exchange_supported else "1-2 hours"
105
+
106
+ # Return a special format that helps the LLM format the response properly
107
+ response = {
108
+ "login_url": login_url,
31
109
  "token_status": token_status,
32
- "token_preview": cached_token[:10] + "...",
33
- "created_at": auth_manager.token_info.created_at if hasattr(auth_manager, "token_info") else None,
34
- "expires_in": auth_manager.token_info.expires_in if hasattr(auth_manager, "token_info") else None
35
- }, indent=2)
36
-
37
- # IMPORTANT: Start the callback server first by calling our helper function
38
- # This ensures the server is ready before we provide the URL to the user
39
- logger.info("Starting callback server for authentication")
40
- port = start_callback_server()
41
- logger.info(f"Callback server started on port {port}")
42
-
43
- # Generate direct login URL
44
- auth_manager.redirect_uri = f"http://localhost:{port}/callback" # Ensure port is set correctly
45
- logger.info(f"Setting redirect URI to {auth_manager.redirect_uri}")
46
- login_url = auth_manager.get_auth_url()
47
- logger.info(f"Generated login URL: {login_url}")
48
-
49
- # Check if we can exchange for long-lived tokens
50
- token_exchange_supported = bool(META_APP_SECRET)
51
- token_duration = "60 days" if token_exchange_supported else "1-2 hours"
52
-
53
- # Return a special format that helps the LLM format the response properly
54
- response = {
55
- "login_url": login_url,
56
- "token_status": token_status,
57
- "server_status": f"Callback server running on port {port}",
58
- "markdown_link": f"[Click here to authenticate with Meta Ads]({login_url})",
59
- "message": "IMPORTANT: Please use the Markdown link format in your response to allow the user to click it.",
60
- "instructions_for_llm": "You must present this link as clickable Markdown to the user using the markdown_link format provided.",
61
- "token_exchange": "enabled" if token_exchange_supported else "disabled",
62
- "token_duration": token_duration,
63
- "token_exchange_message": f"Your authentication token will be valid for approximately {token_duration}." +
64
- (" Long-lived token exchange is enabled." if token_exchange_supported else
65
- " To enable long-lived tokens (60 days), set the META_APP_SECRET environment variable."),
66
- "note": "After authenticating, the token will be automatically saved."
67
- }
68
-
69
- # Wait a moment to ensure the server is fully started
70
- await asyncio.sleep(1)
71
-
72
- return json.dumps(response, indent=2)
110
+ "server_status": f"Callback server running on port {port}",
111
+ "markdown_link": f"[Click here to authenticate with Meta Ads]({login_url})",
112
+ "message": "IMPORTANT: Please use the Markdown link format in your response to allow the user to click it.",
113
+ "instructions_for_llm": "You must present this link as clickable Markdown to the user using the markdown_link format provided.",
114
+ "token_exchange": "enabled" if token_exchange_supported else "disabled",
115
+ "token_duration": token_duration,
116
+ "authentication_method": "meta_oauth",
117
+ "token_exchange_message": f"Your authentication token will be valid for approximately {token_duration}." +
118
+ (" Long-lived token exchange is enabled." if token_exchange_supported else
119
+ " To enable long-lived tokens (60 days), set the META_APP_SECRET environment variable."),
120
+ "note": "After authenticating, the token will be automatically saved."
121
+ }
122
+
123
+ # Wait a moment to ensure the server is fully started
124
+ await asyncio.sleep(1)
125
+
126
+ return json.dumps(response, indent=2)