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.
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.6"
10
+ __version__ = "0.2.9"
11
11
 
12
12
  __all__ = [
13
13
  'get_ad_accounts',
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
@@ -1,7 +1,7 @@
1
1
  """Ad and Creative-related functionality for Meta Ads API."""
2
2
 
3
3
  import json
4
- from typing import Optional, Dict, Any
4
+ from typing import Optional, Dict, Any, List
5
5
  import io
6
6
  from PIL import Image as PILImage
7
7
  from mcp.server.fastmcp import Image
@@ -36,18 +36,27 @@ async def get_ads(access_token: str = None, account_id: str = None, limit: int =
36
36
  else:
37
37
  return json.dumps({"error": "No account ID specified and no accounts found for user"}, indent=2)
38
38
 
39
- endpoint = f"{account_id}/ads"
40
- params = {
41
- "fields": "id,name,adset_id,campaign_id,status,creative,created_time,updated_time,bid_amount,conversion_domain,tracking_specs",
42
- "limit": limit
43
- }
44
-
39
+ # Use campaign-specific endpoint if campaign_id is provided
45
40
  if campaign_id:
46
- params["campaign_id"] = campaign_id
47
-
48
- if adset_id:
49
- params["adset_id"] = adset_id
50
-
41
+ endpoint = f"{campaign_id}/ads"
42
+ params = {
43
+ "fields": "id,name,adset_id,campaign_id,status,creative,created_time,updated_time,bid_amount,conversion_domain,tracking_specs",
44
+ "limit": limit
45
+ }
46
+ # Adset ID can still be used to filter within the campaign
47
+ if adset_id:
48
+ params["adset_id"] = adset_id
49
+ else:
50
+ # Default to account-level endpoint if no campaign_id
51
+ endpoint = f"{account_id}/ads"
52
+ params = {
53
+ "fields": "id,name,adset_id,campaign_id,status,creative,created_time,updated_time,bid_amount,conversion_domain,tracking_specs",
54
+ "limit": limit
55
+ }
56
+ # Adset ID can filter at the account level if no campaign specified
57
+ if adset_id:
58
+ params["adset_id"] = adset_id
59
+
51
60
  data = await make_api_request(endpoint, access_token, params)
52
61
 
53
62
  return json.dumps(data, indent=2)
@@ -78,156 +87,98 @@ async def get_ad_details(access_token: str = None, ad_id: str = None) -> str:
78
87
 
79
88
  @mcp_server.tool()
80
89
  @meta_api_tool
81
- async def get_ad_creatives(access_token: str = None, ad_id: str = None) -> str:
90
+ async def create_ad(
91
+ account_id: str = None,
92
+ name: str = None,
93
+ adset_id: str = None,
94
+ creative_id: str = None,
95
+ status: str = "PAUSED",
96
+ bid_amount = None,
97
+ tracking_specs: Optional[List[Dict[str, Any]]] = None,
98
+ access_token: str = None
99
+ ) -> str:
82
100
  """
83
- Get creative details for a specific ad. Best if combined with get_ad_image to get the full image.
101
+ Create a new ad with an existing creative.
84
102
 
85
103
  Args:
104
+ account_id: Meta Ads account ID (format: act_XXXXXXXXX)
105
+ name: Ad name
106
+ adset_id: Ad set ID where this ad will be placed
107
+ creative_id: ID of an existing creative to use
108
+ status: Initial ad status (default: PAUSED)
109
+ bid_amount: Optional bid amount in account currency (in cents)
110
+ tracking_specs: Optional tracking specifications (e.g., for pixel events).
111
+ Example: [{"action.type":"offsite_conversion","fb_pixel":["YOUR_PIXEL_ID"]}]
86
112
  access_token: Meta API access token (optional - will use cached token if not provided)
87
- ad_id: Meta Ads ad ID
88
113
  """
89
- if not ad_id:
90
- return json.dumps({"error": "No ad ID provided"}, indent=2)
91
-
92
- # First, get the creative ID from the ad
93
- endpoint = f"{ad_id}"
94
- params = {
95
- "fields": "creative"
96
- }
114
+ # Check required parameters
115
+ if not account_id:
116
+ return json.dumps({"error": "No account ID provided"}, indent=2)
97
117
 
98
- ad_data = await make_api_request(endpoint, access_token, params)
118
+ if not name:
119
+ return json.dumps({"error": "No ad name provided"}, indent=2)
99
120
 
100
- if "error" in ad_data:
101
- return json.dumps(ad_data, indent=2)
121
+ if not adset_id:
122
+ return json.dumps({"error": "No ad set ID provided"}, indent=2)
102
123
 
103
- if "creative" not in ad_data:
104
- return json.dumps({"error": "No creative found for this ad"}, indent=2)
105
-
106
- creative_id = ad_data.get("creative", {}).get("id")
107
124
  if not creative_id:
108
- return json.dumps({"error": "Creative ID not found", "ad_data": ad_data}, indent=2)
125
+ return json.dumps({"error": "No creative ID provided"}, indent=2)
109
126
 
110
- # Now get the creative details with essential fields
111
- creative_endpoint = f"{creative_id}"
112
- creative_params = {
113
- "fields": "id,name,title,body,image_url,object_story_spec,url_tags,link_url,thumbnail_url,image_hash,asset_feed_spec,object_type"
114
- }
127
+ endpoint = f"{account_id}/ads"
115
128
 
116
- creative_data = await make_api_request(creative_endpoint, access_token, creative_params)
129
+ params = {
130
+ "name": name,
131
+ "adset_id": adset_id,
132
+ "creative": {"creative_id": creative_id},
133
+ "status": status
134
+ }
117
135
 
118
- # Try to get full-size images in different ways:
136
+ # Add bid amount if provided
137
+ if bid_amount is not None:
138
+ params["bid_amount"] = str(bid_amount)
139
+
140
+ # Add tracking specs if provided
141
+ if tracking_specs is not None:
142
+ params["tracking_specs"] = json.dumps(tracking_specs) # Needs to be JSON encoded string
119
143
 
120
- # 1. First approach: Get ad images directly using the adimages endpoint
121
- if "image_hash" in creative_data:
122
- image_hash = creative_data.get("image_hash")
123
- image_endpoint = f"act_{ad_data.get('account_id', '')}/adimages"
124
- image_params = {
125
- "hashes": [image_hash]
126
- }
127
- image_data = await make_api_request(image_endpoint, access_token, image_params)
128
- if "data" in image_data and len(image_data["data"]) > 0:
129
- creative_data["full_image_url"] = image_data["data"][0].get("url")
144
+ try:
145
+ data = await make_api_request(endpoint, access_token, params, method="POST")
146
+ return json.dumps(data, indent=2)
147
+ except Exception as e:
148
+ error_msg = str(e)
149
+ return json.dumps({
150
+ "error": "Failed to create ad",
151
+ "details": error_msg,
152
+ "params_sent": params
153
+ }, indent=2)
154
+
155
+
156
+ @mcp_server.tool()
157
+ @meta_api_tool
158
+ async def get_ad_creatives(access_token: str = None, ad_id: str = None) -> str:
159
+ """
160
+ Get creative details for a specific ad. Best if combined with get_ad_image to get the full image.
130
161
 
131
- # 2. For creatives with object_story_spec
132
- if "object_story_spec" in creative_data:
133
- spec = creative_data.get("object_story_spec", {})
134
-
135
- # For link ads
136
- if "link_data" in spec:
137
- link_data = spec.get("link_data", {})
138
- # If there's an explicit image_url, use it
139
- if "image_url" in link_data:
140
- creative_data["full_image_url"] = link_data.get("image_url")
141
- # If there's an image_hash, try to get the full image
142
- elif "image_hash" in link_data:
143
- image_hash = link_data.get("image_hash")
144
- account_id = ad_data.get('account_id', '')
145
- if not account_id:
146
- # Try to get account ID from ad ID
147
- ad_details_endpoint = f"{ad_id}"
148
- ad_details_params = {
149
- "fields": "account_id"
150
- }
151
- ad_details = await make_api_request(ad_details_endpoint, access_token, ad_details_params)
152
- account_id = ad_details.get('account_id', '')
153
-
154
- if account_id:
155
- image_endpoint = f"act_{account_id}/adimages"
156
- image_params = {
157
- "hashes": [image_hash]
158
- }
159
- image_data = await make_api_request(image_endpoint, access_token, image_params)
160
- if "data" in image_data and len(image_data["data"]) > 0:
161
- creative_data["full_image_url"] = image_data["data"][0].get("url")
162
+ Args:
163
+ access_token: Meta API access token (optional - will use cached token if not provided)
164
+ ad_id: Meta Ads ad ID
165
+ """
166
+ if not ad_id:
167
+ return json.dumps({"error": "No ad ID provided"}, indent=2)
162
168
 
163
- # For photo ads
164
- if "photo_data" in spec:
165
- photo_data = spec.get("photo_data", {})
166
- if "image_hash" in photo_data:
167
- image_hash = photo_data.get("image_hash")
168
- account_id = ad_data.get('account_id', '')
169
- if not account_id:
170
- # Try to get account ID from ad ID
171
- ad_details_endpoint = f"{ad_id}"
172
- ad_details_params = {
173
- "fields": "account_id"
174
- }
175
- ad_details = await make_api_request(ad_details_endpoint, access_token, ad_details_params)
176
- account_id = ad_details.get('account_id', '')
177
-
178
- if account_id:
179
- image_endpoint = f"act_{account_id}/adimages"
180
- image_params = {
181
- "hashes": [image_hash]
182
- }
183
- image_data = await make_api_request(image_endpoint, access_token, image_params)
184
- if "data" in image_data and len(image_data["data"]) > 0:
185
- creative_data["full_image_url"] = image_data["data"][0].get("url")
186
-
187
- # 3. If there's an asset_feed_spec, try to get images from there
188
- if "asset_feed_spec" in creative_data and "images" in creative_data["asset_feed_spec"]:
189
- images = creative_data["asset_feed_spec"]["images"]
190
- if images and len(images) > 0 and "hash" in images[0]:
191
- image_hash = images[0]["hash"]
192
- account_id = ad_data.get('account_id', '')
193
- if not account_id:
194
- # Try to get account ID
195
- ad_details_endpoint = f"{ad_id}"
196
- ad_details_params = {
197
- "fields": "account_id"
198
- }
199
- ad_details = await make_api_request(ad_details_endpoint, access_token, ad_details_params)
200
- account_id = ad_details.get('account_id', '')
201
-
202
- if account_id:
203
- image_endpoint = f"act_{account_id}/adimages"
204
- image_params = {
205
- "hashes": [image_hash]
206
- }
207
- image_data = await make_api_request(image_endpoint, access_token, image_params)
208
- if "data" in image_data and len(image_data["data"]) > 0:
209
- creative_data["full_image_url"] = image_data["data"][0].get("url")
210
-
211
- # If we have a thumbnail_url but no full_image_url, let's attempt to convert the thumbnail URL to full size
212
- if "thumbnail_url" in creative_data and "full_image_url" not in creative_data:
213
- thumbnail_url = creative_data["thumbnail_url"]
214
- # Try to convert the URL to get higher resolution by removing size parameters
215
- if "p64x64" in thumbnail_url:
216
- full_url = thumbnail_url.replace("p64x64", "p1080x1080")
217
- creative_data["full_image_url"] = full_url
218
- elif "dst-emg0" in thumbnail_url:
219
- # Remove the dst-emg0 parameter that seems to reduce size
220
- full_url = thumbnail_url.replace("dst-emg0_", "")
221
- creative_data["full_image_url"] = full_url
222
-
223
- # Fallback to using thumbnail or image_url if we still don't have a full image
224
- if "full_image_url" not in creative_data:
225
- if "thumbnail_url" in creative_data:
226
- creative_data["full_image_url"] = creative_data["thumbnail_url"]
227
- elif "image_url" in creative_data:
228
- creative_data["full_image_url"] = creative_data["image_url"]
229
-
230
- return json.dumps(creative_data, indent=2)
169
+ endpoint = f"{ad_id}/adcreatives"
170
+ params = {
171
+ "fields": "id,name,status,thumbnail_url,image_url,image_hash,object_story_spec" # Added image_hash
172
+ }
173
+
174
+ data = await make_api_request(endpoint, access_token, params)
175
+
176
+ # Add image URLs for direct viewing if available
177
+ if 'data' in data:
178
+ for creative in data['data']:
179
+ creative['image_urls_for_viewing'] = ad_creative_images(creative)
180
+
181
+ return json.dumps(data, indent=2)
231
182
 
232
183
 
233
184
  @mcp_server.tool()
@@ -368,7 +319,13 @@ async def get_ad_image(access_token: str = None, ad_id: str = None) -> Image:
368
319
 
369
320
  @mcp_server.tool()
370
321
  @meta_api_tool
371
- async def update_ad(ad_id: str, status: str = None, bid_amount: int = None, access_token: str = None) -> str:
322
+ async def update_ad(
323
+ ad_id: str,
324
+ status: str = None,
325
+ bid_amount: int = None,
326
+ tracking_specs = None,
327
+ access_token: str = None
328
+ ) -> str:
372
329
  """
373
330
  Update an ad with new settings.
374
331
 
@@ -376,51 +333,25 @@ async def update_ad(ad_id: str, status: str = None, bid_amount: int = None, acce
376
333
  ad_id: Meta Ads ad ID
377
334
  status: Update ad status (ACTIVE, PAUSED, etc.)
378
335
  bid_amount: Bid amount in account currency (in cents for USD)
336
+ tracking_specs: Optional tracking specifications (e.g., for pixel events).
379
337
  access_token: Meta API access token (optional - will use cached token if not provided)
380
338
  """
381
339
  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
-
340
+ return json.dumps({"error": "Ad ID is required"}, indent=2)
341
+
342
+ params = {}
343
+ if status:
344
+ params["status"] = status
389
345
  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)
346
+ # Ensure bid_amount is sent as a string if it's not null
347
+ params["bid_amount"] = str(bid_amount)
348
+ if tracking_specs is not None: # Add tracking_specs to params if provided
349
+ params["tracking_specs"] = json.dumps(tracking_specs) # Needs to be JSON encoded string
350
+
351
+ if not params:
352
+ return json.dumps({"error": "No update parameters provided (status, bid_amount, or tracking_specs)"}, indent=2)
353
+
354
+ endpoint = f"{ad_id}"
355
+ data = await make_api_request(endpoint, access_token, params, method='POST')
356
+
357
+ return json.dumps(data, indent=2)