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 +1 -1
- meta_ads_mcp/api.py +122 -53
- meta_ads_mcp/core/__init__.py +2 -1
- meta_ads_mcp/core/ads.py +123 -192
- meta_ads_mcp/core/adsets.py +137 -11
- meta_ads_mcp/core/api.py +29 -1
- meta_ads_mcp/core/auth.py +83 -15
- meta_ads_mcp/core/authentication.py +103 -49
- meta_ads_mcp/core/campaigns.py +151 -14
- meta_ads_mcp/core/insights.py +18 -4
- meta_ads_mcp/core/pipeboard_auth.py +484 -0
- meta_ads_mcp/core/server.py +49 -4
- meta_ads_mcp/core/utils.py +11 -5
- {meta_ads_mcp-0.2.6.dist-info → meta_ads_mcp-0.2.9.dist-info}/METADATA +122 -31
- meta_ads_mcp-0.2.9.dist-info/RECORD +22 -0
- meta_ads_mcp-0.2.9.dist-info/licenses/LICENSE +201 -0
- meta_ads_mcp-0.2.6.dist-info/RECORD +0 -20
- {meta_ads_mcp-0.2.6.dist-info → meta_ads_mcp-0.2.9.dist-info}/WHEEL +0 -0
- {meta_ads_mcp-0.2.6.dist-info → meta_ads_mcp-0.2.9.dist-info}/entry_points.txt +0 -0
meta_ads_mcp/__init__.py
CHANGED
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
|
-
#
|
|
525
|
-
|
|
524
|
+
# Check if we're using Pipeboard authentication
|
|
525
|
+
using_pipeboard = bool(os.environ.get("PIPEBOARD_API_TOKEN", ""))
|
|
526
526
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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
|
-
#
|
|
583
|
-
|
|
584
|
-
"error": "
|
|
585
|
-
"
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
-
|
|
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):
|
meta_ads_mcp/core/__init__.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
118
|
+
if not name:
|
|
119
|
+
return json.dumps({"error": "No ad name provided"}, indent=2)
|
|
99
120
|
|
|
100
|
-
if
|
|
101
|
-
return json.dumps(
|
|
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": "
|
|
125
|
+
return json.dumps({"error": "No creative ID provided"}, indent=2)
|
|
109
126
|
|
|
110
|
-
|
|
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
|
-
|
|
129
|
+
params = {
|
|
130
|
+
"name": name,
|
|
131
|
+
"adset_id": adset_id,
|
|
132
|
+
"creative": {"creative_id": creative_id},
|
|
133
|
+
"status": status
|
|
134
|
+
}
|
|
117
135
|
|
|
118
|
-
#
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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(
|
|
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": "
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
|
|
391
|
-
|
|
392
|
-
if not
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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)
|