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.
meta_ads_mcp/__init__.py CHANGED
@@ -7,7 +7,7 @@ with the Claude LLM.
7
7
 
8
8
  from meta_ads_mcp.core.server import main
9
9
 
10
- __version__ = "0.2.5"
10
+ __version__ = "0.2.8"
11
11
 
12
12
  __all__ = [
13
13
  'get_ad_accounts',
@@ -22,6 +22,7 @@ __all__ = [
22
22
  'get_ad_details',
23
23
  'get_ad_creatives',
24
24
  'get_ad_image',
25
+ 'update_ad',
25
26
  'get_insights',
26
27
  'debug_image_download',
27
28
  'get_login_link',
@@ -43,6 +44,7 @@ from .core import (
43
44
  get_ad_details,
44
45
  get_ad_creatives,
45
46
  get_ad_image,
47
+ update_ad,
46
48
  get_insights,
47
49
  debug_image_download,
48
50
  get_login_link,
meta_ads_mcp/api.py CHANGED
@@ -30,7 +30,7 @@ USER_AGENT = "meta-ads-mcp/1.0"
30
30
  META_APP_ID = os.environ.get("META_APP_ID", "") # Default to empty string
31
31
 
32
32
  # Auth constants
33
- AUTH_SCOPE = "ads_management,ads_read,business_management"
33
+ AUTH_SCOPE = "ads_management,ads_read,business_management,public_profile"
34
34
  AUTH_REDIRECT_URI = "http://localhost:8888/callback"
35
35
  AUTH_RESPONSE_TYPE = "token"
36
36
 
@@ -521,45 +521,40 @@ def meta_api_tool(func):
521
521
  if not access_token:
522
522
  needs_authentication = True
523
523
 
524
- # Start the callback server
525
- port = start_callback_server()
524
+ # Check if we're using Pipeboard authentication
525
+ using_pipeboard = bool(os.environ.get("PIPEBOARD_API_TOKEN", ""))
526
526
 
527
- # Get current app ID from config
528
- current_app_id = meta_config.get_app_id()
529
- if not current_app_id:
530
- return json.dumps({
531
- "error": "No Meta App ID provided. Please provide a valid app ID via environment variable META_APP_ID or --app-id CLI argument.",
532
- "help": "This is required for authentication with Meta Graph API."
533
- }, indent=2)
534
-
535
- # Update auth manager with current app ID
536
- auth_manager.app_id = current_app_id
537
- print(f"Using Meta App ID from config: {current_app_id}")
538
-
539
- # Update auth manager's redirect URI with the current port
540
- auth_manager.redirect_uri = f"http://localhost:{port}/callback"
541
-
542
- # Generate the authentication URL
543
- login_url = auth_manager.get_auth_url()
544
-
545
- # Return a user-friendly authentication required response
546
- return json.dumps({
547
- "error": "Authentication Required",
548
- "details": {
549
- "message": "You need to authenticate with the Meta API before using this tool",
550
- "action_required": "Please authenticate using the link below",
551
- "login_url": login_url,
552
- "markdown_link": f"[Click here to authenticate with Meta Ads API]({login_url})"
553
- }
554
- }, indent=2)
555
-
556
- # Call the original function
557
- try:
558
- result = await func(**kwargs)
559
-
560
- # If authentication is needed after the call (e.g., token was invalidated)
561
- if needs_authentication:
562
- # Start the callback server
527
+ if using_pipeboard:
528
+ # For Pipeboard, we use a different authentication flow
529
+ try:
530
+ # Here we'd import dynamically to avoid circular imports
531
+ from .core.pipeboard_auth import pipeboard_auth_manager
532
+
533
+ # Initiate the Pipeboard auth flow
534
+ auth_data = pipeboard_auth_manager.initiate_auth_flow()
535
+ login_url = auth_data.get("loginUrl")
536
+
537
+ # Return a user-friendly authentication required response for Pipeboard
538
+ return json.dumps({
539
+ "error": "Authentication Required",
540
+ "details": {
541
+ "message": "You need to authenticate with the Meta API via Pipeboard",
542
+ "action_required": "Please authenticate using the link below",
543
+ "login_url": login_url,
544
+ "markdown_link": f"[Click here to authenticate with Meta Ads API via Pipeboard]({login_url})",
545
+ "authentication_method": "pipeboard"
546
+ }
547
+ }, indent=2)
548
+ except Exception as e:
549
+ return json.dumps({
550
+ "error": f"Pipeboard Authentication Error: {str(e)}",
551
+ "details": {
552
+ "message": "Failed to initiate Pipeboard authentication flow",
553
+ "action_required": "Please check your PIPEBOARD_API_TOKEN environment variable"
554
+ }
555
+ }, indent=2)
556
+ else:
557
+ # For direct Meta auth, start the callback server
563
558
  port = start_callback_server()
564
559
 
565
560
  # Get current app ID from config
@@ -570,6 +565,7 @@ def meta_api_tool(func):
570
565
  "help": "This is required for authentication with Meta Graph API."
571
566
  }, indent=2)
572
567
 
568
+ # Update auth manager with current app ID
573
569
  auth_manager.app_id = current_app_id
574
570
  print(f"Using Meta App ID from config: {current_app_id}")
575
571
 
@@ -579,21 +575,94 @@ def meta_api_tool(func):
579
575
  # Generate the authentication URL
580
576
  login_url = auth_manager.get_auth_url()
581
577
 
582
- # Create a resource response that includes the markdown link format
583
- response = {
584
- "error": "Session expired or token invalid. Please re-authenticate with Meta Ads API",
585
- "login_url": login_url,
586
- "server_status": f"Callback server running on port {port}",
587
- "markdown_link": f"[Click here to re-authenticate with Meta Ads API]({login_url})",
588
- "message": "IMPORTANT: Please use the Markdown link format in your response to allow the user to click it.",
589
- "instructions_for_llm": "You must present this link as clickable Markdown to the user using the markdown_link format provided.",
590
- "note": "After authenticating, the token will be automatically saved."
591
- }
592
-
593
- # Wait a moment to ensure the server is fully started
594
- await asyncio.sleep(1)
578
+ # Return a user-friendly authentication required response
579
+ return json.dumps({
580
+ "error": "Authentication Required",
581
+ "details": {
582
+ "message": "You need to authenticate with the Meta API before using this tool",
583
+ "action_required": "Please authenticate using the link below",
584
+ "login_url": login_url,
585
+ "markdown_link": f"[Click here to authenticate with Meta Ads API]({login_url})",
586
+ "authentication_method": "meta_oauth"
587
+ }
588
+ }, indent=2)
589
+
590
+ # Call the original function
591
+ try:
592
+ result = await func(**kwargs)
593
+
594
+ # If authentication is needed after the call (e.g., token was invalidated)
595
+ if needs_authentication:
596
+ # Check if we're using Pipeboard authentication
597
+ using_pipeboard = bool(os.environ.get("PIPEBOARD_API_TOKEN", ""))
595
598
 
596
- return json.dumps(response, indent=2)
599
+ if using_pipeboard:
600
+ # For Pipeboard, we use a different authentication flow
601
+ try:
602
+ # Here we'd import dynamically to avoid circular imports
603
+ from .core.pipeboard_auth import pipeboard_auth_manager
604
+
605
+ # Initiate the Pipeboard auth flow
606
+ auth_data = pipeboard_auth_manager.initiate_auth_flow()
607
+ login_url = auth_data.get("loginUrl")
608
+
609
+ # Create a resource response that includes the markdown link format
610
+ response = {
611
+ "error": "Session expired or token invalid. Please re-authenticate with Meta Ads API",
612
+ "login_url": login_url,
613
+ "markdown_link": f"[Click here to re-authenticate with Meta Ads API via Pipeboard]({login_url})",
614
+ "message": "IMPORTANT: Please use the Markdown link format in your response to allow the user to click it.",
615
+ "instructions_for_llm": "You must present this link as clickable Markdown to the user using the markdown_link format provided.",
616
+ "authentication_method": "pipeboard",
617
+ "note": "After authenticating, the token will be automatically saved."
618
+ }
619
+
620
+ return json.dumps(response, indent=2)
621
+ except Exception as e:
622
+ return json.dumps({
623
+ "error": f"Pipeboard Authentication Error: {str(e)}",
624
+ "details": {
625
+ "message": "Failed to initiate Pipeboard authentication flow",
626
+ "action_required": "Please check your PIPEBOARD_API_TOKEN environment variable"
627
+ }
628
+ }, indent=2)
629
+ else:
630
+ # For direct Meta auth, start the callback server
631
+ port = start_callback_server()
632
+
633
+ # Get current app ID from config
634
+ current_app_id = meta_config.get_app_id()
635
+ if not current_app_id:
636
+ return json.dumps({
637
+ "error": "No Meta App ID provided. Please provide a valid app ID via environment variable META_APP_ID or --app-id CLI argument.",
638
+ "help": "This is required for authentication with Meta Graph API."
639
+ }, indent=2)
640
+
641
+ auth_manager.app_id = current_app_id
642
+ print(f"Using Meta App ID from config: {current_app_id}")
643
+
644
+ # Update auth manager's redirect URI with the current port
645
+ auth_manager.redirect_uri = f"http://localhost:{port}/callback"
646
+
647
+ # Generate the authentication URL
648
+ login_url = auth_manager.get_auth_url()
649
+
650
+ # Create a resource response that includes the markdown link format
651
+ response = {
652
+ "error": "Session expired or token invalid. Please re-authenticate with Meta Ads API",
653
+ "login_url": login_url,
654
+ "server_status": f"Callback server running on port {port}",
655
+ "markdown_link": f"[Click here to re-authenticate with Meta Ads API]({login_url})",
656
+ "message": "IMPORTANT: Please use the Markdown link format in your response to allow the user to click it.",
657
+ "instructions_for_llm": "You must present this link as clickable Markdown to the user using the markdown_link format provided.",
658
+ "authentication_method": "meta_oauth",
659
+ "note": "After authenticating, the token will be automatically saved."
660
+ }
661
+
662
+ # Wait a moment to ensure the server is fully started
663
+ await asyncio.sleep(1)
664
+
665
+ return json.dumps(response, indent=2)
597
666
 
598
667
  # If result is a string (JSON), check for app ID errors and improve them
599
668
  if isinstance(result, str):
@@ -4,7 +4,7 @@ from .server import mcp_server
4
4
  from .accounts import get_ad_accounts, get_account_info
5
5
  from .campaigns import get_campaigns, get_campaign_details, create_campaign
6
6
  from .adsets import get_adsets, get_adset_details, update_adset
7
- from .ads import get_ads, get_ad_details, get_ad_creatives, get_ad_image
7
+ from .ads import get_ads, get_ad_details, get_ad_creatives, get_ad_image, update_ad
8
8
  from .insights import get_insights, debug_image_download
9
9
  from .authentication import get_login_link
10
10
  from .server import login_cli, main
@@ -24,6 +24,7 @@ __all__ = [
24
24
  'get_ad_details',
25
25
  'get_ad_creatives',
26
26
  'get_ad_image',
27
+ 'update_ad',
27
28
  'get_insights',
28
29
  'debug_image_download',
29
30
  'get_login_link',
meta_ads_mcp/core/ads.py CHANGED
@@ -363,4 +363,64 @@ async def get_ad_image(access_token: str = None, ad_id: str = None) -> Image:
363
363
  return Image(data=img_bytes, format="jpeg")
364
364
 
365
365
  except Exception as e:
366
- return f"Error processing image: {str(e)}"
366
+ return f"Error processing image: {str(e)}"
367
+
368
+
369
+ @mcp_server.tool()
370
+ @meta_api_tool
371
+ async def update_ad(ad_id: str, status: str = None, bid_amount: int = None, access_token: str = None) -> str:
372
+ """
373
+ Update an ad with new settings.
374
+
375
+ Args:
376
+ ad_id: Meta Ads ad ID
377
+ status: Update ad status (ACTIVE, PAUSED, etc.)
378
+ bid_amount: Bid amount in account currency (in cents for USD)
379
+ access_token: Meta API access token (optional - will use cached token if not provided)
380
+ """
381
+ if not ad_id:
382
+ return json.dumps({"error": "No ad ID provided"}, indent=2)
383
+
384
+ changes = {}
385
+
386
+ if status is not None:
387
+ changes['status'] = status
388
+
389
+ if bid_amount is not None:
390
+ changes['bid_amount'] = bid_amount
391
+
392
+ if not changes:
393
+ return json.dumps({"error": "No update parameters provided"}, indent=2)
394
+
395
+ # Get current ad details for comparison
396
+ current_details_json = await get_ad_details(ad_id=ad_id, access_token=access_token)
397
+ current_details = json.loads(current_details_json)
398
+
399
+ # Import the callback server components
400
+ from .callback_server import start_callback_server, update_confirmation
401
+ import urllib.parse
402
+
403
+ # Start the callback server if not already running
404
+ port = start_callback_server()
405
+
406
+ # Generate confirmation URL with properly encoded parameters
407
+ changes_json = json.dumps(changes)
408
+ encoded_changes = urllib.parse.quote(changes_json)
409
+ confirmation_url = f"http://localhost:{port}/confirm-update?ad_id={ad_id}&token={access_token}&changes={encoded_changes}"
410
+
411
+ # Reset the update confirmation
412
+ update_confirmation.clear()
413
+ update_confirmation.update({"approved": False})
414
+
415
+ # Return the confirmation link
416
+ response = {
417
+ "message": "Please confirm the ad update",
418
+ "confirmation_url": confirmation_url,
419
+ "markdown_link": f"[Click here to confirm ad update]({confirmation_url})",
420
+ "current_details": current_details,
421
+ "proposed_changes": changes,
422
+ "instructions_for_llm": "You must present this link as clickable Markdown to the user using the markdown_link format provided.",
423
+ "note": "After authenticating, the token will be automatically saved and your ad will be updated. Refresh the browser page if it doesn't load immediately."
424
+ }
425
+
426
+ return json.dumps(response, indent=2)
@@ -6,7 +6,7 @@ from .api import meta_api_tool, make_api_request
6
6
  from .accounts import get_ad_accounts
7
7
  from .server import mcp_server
8
8
  import asyncio
9
- from .auth import start_callback_server, update_confirmation
9
+ from .callback_server import start_callback_server, update_confirmation
10
10
  import urllib.parse
11
11
 
12
12
 
@@ -78,7 +78,8 @@ async def get_adset_details(access_token: str = None, adset_id: str = None) -> s
78
78
  @mcp_server.tool()
79
79
  @meta_api_tool
80
80
  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:
81
+ bid_amount: int = None, status: str = None, targeting: Dict[str, Any] = None,
82
+ optimization_goal: str = None, access_token: str = None) -> str:
82
83
  """
83
84
  Update an ad set with new settings including frequency caps.
84
85
 
@@ -91,6 +92,7 @@ async def update_adset(adset_id: str, frequency_control_specs: List[Dict[str, An
91
92
  status: Update ad set status (ACTIVE, PAUSED, etc.)
92
93
  targeting: Targeting specifications including targeting_automation
93
94
  (e.g. {"targeting_automation":{"advantage_audience":1}})
95
+ optimization_goal: Conversion optimization goal (e.g., 'LINK_CLICKS', 'CONVERSIONS', 'APP_INSTALLS', etc.)
94
96
  access_token: Meta API access token (optional - will use cached token if not provided)
95
97
  """
96
98
  if not adset_id:
@@ -110,6 +112,9 @@ async def update_adset(adset_id: str, frequency_control_specs: List[Dict[str, An
110
112
  if status is not None:
111
113
  changes['status'] = status
112
114
 
115
+ if optimization_goal is not None:
116
+ changes['optimization_goal'] = optimization_goal
117
+
113
118
  if targeting is not None:
114
119
  # Get current ad set details to preserve existing targeting settings
115
120
  current_details_json = await get_adset_details(adset_id=adset_id, access_token=access_token)
@@ -163,7 +168,7 @@ async def update_adset(adset_id: str, frequency_control_specs: List[Dict[str, An
163
168
  "current_details": current_details,
164
169
  "proposed_changes": changes,
165
170
  "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."
171
+ "note": "Click the link to confirm and apply your ad set updates. Refresh the browser page if it doesn't load immediately."
167
172
  }
168
173
 
169
174
  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
  }