meta-ads-mcp 0.2.1__py3-none-any.whl → 0.2.3__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 +19 -5
- meta_ads_mcp/__main__.py +10 -0
- meta_ads_mcp/core/__init__.py +34 -0
- meta_ads_mcp/core/accounts.py +59 -0
- meta_ads_mcp/core/ads.py +361 -0
- meta_ads_mcp/core/adsets.py +115 -0
- meta_ads_mcp/core/api.py +211 -0
- meta_ads_mcp/core/auth.py +416 -0
- meta_ads_mcp/core/authentication.py +56 -0
- meta_ads_mcp/core/campaigns.py +119 -0
- meta_ads_mcp/core/insights.py +412 -0
- meta_ads_mcp/core/resources.py +46 -0
- meta_ads_mcp/core/server.py +56 -0
- meta_ads_mcp/core/utils.py +128 -0
- {meta_ads_mcp-0.2.1.dist-info → meta_ads_mcp-0.2.3.dist-info}/METADATA +1 -1
- meta_ads_mcp-0.2.3.dist-info/RECORD +19 -0
- meta_ads_mcp-0.2.1.dist-info/RECORD +0 -6
- {meta_ads_mcp-0.2.1.dist-info → meta_ads_mcp-0.2.3.dist-info}/WHEEL +0 -0
- {meta_ads_mcp-0.2.1.dist-info → meta_ads_mcp-0.2.3.dist-info}/entry_points.txt +0 -0
meta_ads_mcp/__init__.py
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""
|
|
2
|
+
Meta Ads MCP - Python Package
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
This package provides a Meta Ads Marketing Cloud Platform (MCP) integration
|
|
5
|
+
with the Claude LLM.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from meta_ads_mcp.core.server import main
|
|
9
|
+
|
|
10
|
+
__version__ = "0.2.3"
|
|
11
|
+
|
|
12
|
+
__all__ = ["main"]
|
|
4
13
|
|
|
5
14
|
# Import key functions to make them available at package level
|
|
6
|
-
from .
|
|
15
|
+
from .core import (
|
|
7
16
|
get_ad_accounts,
|
|
8
17
|
get_account_info,
|
|
9
18
|
get_campaigns,
|
|
@@ -11,17 +20,22 @@ from .api import (
|
|
|
11
20
|
create_campaign,
|
|
12
21
|
get_adsets,
|
|
13
22
|
get_adset_details,
|
|
23
|
+
update_adset,
|
|
14
24
|
get_ads,
|
|
15
25
|
get_ad_details,
|
|
16
26
|
get_ad_creatives,
|
|
17
27
|
get_ad_image,
|
|
18
28
|
get_insights,
|
|
29
|
+
debug_image_download,
|
|
30
|
+
save_ad_image_via_api,
|
|
19
31
|
get_login_link,
|
|
20
32
|
login_cli,
|
|
21
|
-
main, # Import main function for entry point
|
|
22
33
|
)
|
|
23
34
|
|
|
24
35
|
# Define a main function to be used as a package entry point
|
|
25
36
|
def entrypoint():
|
|
26
37
|
"""Main entry point for the package when invoked with uvx."""
|
|
27
|
-
return main()
|
|
38
|
+
return main()
|
|
39
|
+
|
|
40
|
+
# Re-export main for direct access
|
|
41
|
+
main = main
|
meta_ads_mcp/__main__.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Core functionality for Meta Ads API MCP package."""
|
|
2
|
+
|
|
3
|
+
from .server import mcp_server
|
|
4
|
+
from .accounts import get_ad_accounts, get_account_info
|
|
5
|
+
from .campaigns import get_campaigns, get_campaign_details, create_campaign
|
|
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
|
|
8
|
+
from .insights import get_insights, debug_image_download, save_ad_image_via_api
|
|
9
|
+
from .authentication import get_login_link
|
|
10
|
+
from .server import login_cli, main
|
|
11
|
+
from .auth import login
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
'mcp_server',
|
|
15
|
+
'get_ad_accounts',
|
|
16
|
+
'get_account_info',
|
|
17
|
+
'get_campaigns',
|
|
18
|
+
'get_campaign_details',
|
|
19
|
+
'create_campaign',
|
|
20
|
+
'get_adsets',
|
|
21
|
+
'get_adset_details',
|
|
22
|
+
'update_adset',
|
|
23
|
+
'get_ads',
|
|
24
|
+
'get_ad_details',
|
|
25
|
+
'get_ad_creatives',
|
|
26
|
+
'get_ad_image',
|
|
27
|
+
'get_insights',
|
|
28
|
+
'debug_image_download',
|
|
29
|
+
'save_ad_image_via_api',
|
|
30
|
+
'get_login_link',
|
|
31
|
+
'login_cli',
|
|
32
|
+
'login',
|
|
33
|
+
'main',
|
|
34
|
+
]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Account-related functionality for Meta Ads API."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from .api import meta_api_tool, make_api_request
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@meta_api_tool
|
|
9
|
+
async def get_ad_accounts(access_token: str = None, user_id: str = "me", limit: int = 10) -> str:
|
|
10
|
+
"""
|
|
11
|
+
Get ad accounts accessible by a user.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
15
|
+
user_id: Meta user ID or "me" for the current user
|
|
16
|
+
limit: Maximum number of accounts to return (default: 10)
|
|
17
|
+
"""
|
|
18
|
+
endpoint = f"{user_id}/adaccounts"
|
|
19
|
+
params = {
|
|
20
|
+
"fields": "id,name,account_id,account_status,amount_spent,balance,currency,age,business_city,business_country_code",
|
|
21
|
+
"limit": limit
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
data = await make_api_request(endpoint, access_token, params)
|
|
25
|
+
|
|
26
|
+
return json.dumps(data, indent=2)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@meta_api_tool
|
|
30
|
+
async def get_account_info(access_token: str = None, account_id: str = None) -> str:
|
|
31
|
+
"""
|
|
32
|
+
Get detailed information about a specific ad account.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
36
|
+
account_id: Meta Ads account ID (format: act_XXXXXXXXX)
|
|
37
|
+
"""
|
|
38
|
+
# If no account ID is specified, try to get the first one for the user
|
|
39
|
+
if not account_id:
|
|
40
|
+
accounts_json = await get_ad_accounts("me", json.dumps({"limit": 1}), access_token)
|
|
41
|
+
accounts_data = json.loads(accounts_json)
|
|
42
|
+
|
|
43
|
+
if "data" in accounts_data and accounts_data["data"]:
|
|
44
|
+
account_id = accounts_data["data"][0]["id"]
|
|
45
|
+
else:
|
|
46
|
+
return json.dumps({"error": "No account ID specified and no accounts found for user"}, indent=2)
|
|
47
|
+
|
|
48
|
+
# Ensure account_id has the 'act_' prefix for API compatibility
|
|
49
|
+
if not account_id.startswith("act_"):
|
|
50
|
+
account_id = f"act_{account_id}"
|
|
51
|
+
|
|
52
|
+
endpoint = f"{account_id}"
|
|
53
|
+
params = {
|
|
54
|
+
"fields": "id,name,account_id,account_status,amount_spent,balance,currency,age,funding_source_details,business_city,business_country_code,timezone_name,owner"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
data = await make_api_request(endpoint, access_token, params)
|
|
58
|
+
|
|
59
|
+
return json.dumps(data, indent=2)
|
meta_ads_mcp/core/ads.py
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
"""Ad and Creative-related functionality for Meta Ads API."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Optional, Dict, Any
|
|
5
|
+
import io
|
|
6
|
+
from PIL import Image as PILImage
|
|
7
|
+
from mcp.server.fastmcp import Image
|
|
8
|
+
|
|
9
|
+
from .api import meta_api_tool, make_api_request
|
|
10
|
+
from .accounts import get_ad_accounts
|
|
11
|
+
from .utils import download_image, try_multiple_download_methods, ad_creative_images
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@meta_api_tool
|
|
15
|
+
async def get_ads(access_token: str = None, account_id: str = None, limit: int = 10,
|
|
16
|
+
campaign_id: str = "", adset_id: str = "") -> str:
|
|
17
|
+
"""
|
|
18
|
+
Get ads for a Meta Ads account with optional filtering.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
22
|
+
account_id: Meta Ads account ID (format: act_XXXXXXXXX)
|
|
23
|
+
limit: Maximum number of ads to return (default: 10)
|
|
24
|
+
campaign_id: Optional campaign ID to filter by
|
|
25
|
+
adset_id: Optional ad set ID to filter by
|
|
26
|
+
"""
|
|
27
|
+
# If no account ID is specified, try to get the first one for the user
|
|
28
|
+
if not account_id:
|
|
29
|
+
accounts_json = await get_ad_accounts("me", json.dumps({"limit": 1}), access_token)
|
|
30
|
+
accounts_data = json.loads(accounts_json)
|
|
31
|
+
|
|
32
|
+
if "data" in accounts_data and accounts_data["data"]:
|
|
33
|
+
account_id = accounts_data["data"][0]["id"]
|
|
34
|
+
else:
|
|
35
|
+
return json.dumps({"error": "No account ID specified and no accounts found for user"}, indent=2)
|
|
36
|
+
|
|
37
|
+
endpoint = f"{account_id}/ads"
|
|
38
|
+
params = {
|
|
39
|
+
"fields": "id,name,adset_id,campaign_id,status,creative,created_time,updated_time,bid_amount,conversion_domain,tracking_specs",
|
|
40
|
+
"limit": limit
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if campaign_id:
|
|
44
|
+
params["campaign_id"] = campaign_id
|
|
45
|
+
|
|
46
|
+
if adset_id:
|
|
47
|
+
params["adset_id"] = adset_id
|
|
48
|
+
|
|
49
|
+
data = await make_api_request(endpoint, access_token, params)
|
|
50
|
+
|
|
51
|
+
return json.dumps(data, indent=2)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@meta_api_tool
|
|
55
|
+
async def get_ad_details(access_token: str = None, ad_id: str = None) -> str:
|
|
56
|
+
"""
|
|
57
|
+
Get detailed information about a specific ad.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
61
|
+
ad_id: Meta Ads ad ID
|
|
62
|
+
"""
|
|
63
|
+
if not ad_id:
|
|
64
|
+
return json.dumps({"error": "No ad ID provided"}, indent=2)
|
|
65
|
+
|
|
66
|
+
endpoint = f"{ad_id}"
|
|
67
|
+
params = {
|
|
68
|
+
"fields": "id,name,adset_id,campaign_id,status,creative,created_time,updated_time,bid_amount,conversion_domain,tracking_specs,preview_shareable_link"
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
data = await make_api_request(endpoint, access_token, params)
|
|
72
|
+
|
|
73
|
+
return json.dumps(data, indent=2)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@meta_api_tool
|
|
77
|
+
async def get_ad_creatives(access_token: str = None, ad_id: str = None) -> str:
|
|
78
|
+
"""
|
|
79
|
+
Get creative details for a specific ad. Best if combined with get_ad_image to get the full image.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
83
|
+
ad_id: Meta Ads ad ID
|
|
84
|
+
"""
|
|
85
|
+
if not ad_id:
|
|
86
|
+
return json.dumps({"error": "No ad ID provided"}, indent=2)
|
|
87
|
+
|
|
88
|
+
# First, get the creative ID from the ad
|
|
89
|
+
endpoint = f"{ad_id}"
|
|
90
|
+
params = {
|
|
91
|
+
"fields": "creative"
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
ad_data = await make_api_request(endpoint, access_token, params)
|
|
95
|
+
|
|
96
|
+
if "error" in ad_data:
|
|
97
|
+
return json.dumps(ad_data, indent=2)
|
|
98
|
+
|
|
99
|
+
if "creative" not in ad_data:
|
|
100
|
+
return json.dumps({"error": "No creative found for this ad"}, indent=2)
|
|
101
|
+
|
|
102
|
+
creative_id = ad_data.get("creative", {}).get("id")
|
|
103
|
+
if not creative_id:
|
|
104
|
+
return json.dumps({"error": "Creative ID not found", "ad_data": ad_data}, indent=2)
|
|
105
|
+
|
|
106
|
+
# Now get the creative details with essential fields
|
|
107
|
+
creative_endpoint = f"{creative_id}"
|
|
108
|
+
creative_params = {
|
|
109
|
+
"fields": "id,name,title,body,image_url,object_story_spec,url_tags,link_url,thumbnail_url,image_hash,asset_feed_spec,object_type"
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
creative_data = await make_api_request(creative_endpoint, access_token, creative_params)
|
|
113
|
+
|
|
114
|
+
# Try to get full-size images in different ways:
|
|
115
|
+
|
|
116
|
+
# 1. First approach: Get ad images directly using the adimages endpoint
|
|
117
|
+
if "image_hash" in creative_data:
|
|
118
|
+
image_hash = creative_data.get("image_hash")
|
|
119
|
+
image_endpoint = f"act_{ad_data.get('account_id', '')}/adimages"
|
|
120
|
+
image_params = {
|
|
121
|
+
"hashes": [image_hash]
|
|
122
|
+
}
|
|
123
|
+
image_data = await make_api_request(image_endpoint, access_token, image_params)
|
|
124
|
+
if "data" in image_data and len(image_data["data"]) > 0:
|
|
125
|
+
creative_data["full_image_url"] = image_data["data"][0].get("url")
|
|
126
|
+
|
|
127
|
+
# 2. For creatives with object_story_spec
|
|
128
|
+
if "object_story_spec" in creative_data:
|
|
129
|
+
spec = creative_data.get("object_story_spec", {})
|
|
130
|
+
|
|
131
|
+
# For link ads
|
|
132
|
+
if "link_data" in spec:
|
|
133
|
+
link_data = spec.get("link_data", {})
|
|
134
|
+
# If there's an explicit image_url, use it
|
|
135
|
+
if "image_url" in link_data:
|
|
136
|
+
creative_data["full_image_url"] = link_data.get("image_url")
|
|
137
|
+
# If there's an image_hash, try to get the full image
|
|
138
|
+
elif "image_hash" in link_data:
|
|
139
|
+
image_hash = link_data.get("image_hash")
|
|
140
|
+
account_id = ad_data.get('account_id', '')
|
|
141
|
+
if not account_id:
|
|
142
|
+
# Try to get account ID from ad ID
|
|
143
|
+
ad_details_endpoint = f"{ad_id}"
|
|
144
|
+
ad_details_params = {
|
|
145
|
+
"fields": "account_id"
|
|
146
|
+
}
|
|
147
|
+
ad_details = await make_api_request(ad_details_endpoint, access_token, ad_details_params)
|
|
148
|
+
account_id = ad_details.get('account_id', '')
|
|
149
|
+
|
|
150
|
+
if account_id:
|
|
151
|
+
image_endpoint = f"act_{account_id}/adimages"
|
|
152
|
+
image_params = {
|
|
153
|
+
"hashes": [image_hash]
|
|
154
|
+
}
|
|
155
|
+
image_data = await make_api_request(image_endpoint, access_token, image_params)
|
|
156
|
+
if "data" in image_data and len(image_data["data"]) > 0:
|
|
157
|
+
creative_data["full_image_url"] = image_data["data"][0].get("url")
|
|
158
|
+
|
|
159
|
+
# For photo ads
|
|
160
|
+
if "photo_data" in spec:
|
|
161
|
+
photo_data = spec.get("photo_data", {})
|
|
162
|
+
if "image_hash" in photo_data:
|
|
163
|
+
image_hash = photo_data.get("image_hash")
|
|
164
|
+
account_id = ad_data.get('account_id', '')
|
|
165
|
+
if not account_id:
|
|
166
|
+
# Try to get account ID from ad ID
|
|
167
|
+
ad_details_endpoint = f"{ad_id}"
|
|
168
|
+
ad_details_params = {
|
|
169
|
+
"fields": "account_id"
|
|
170
|
+
}
|
|
171
|
+
ad_details = await make_api_request(ad_details_endpoint, access_token, ad_details_params)
|
|
172
|
+
account_id = ad_details.get('account_id', '')
|
|
173
|
+
|
|
174
|
+
if account_id:
|
|
175
|
+
image_endpoint = f"act_{account_id}/adimages"
|
|
176
|
+
image_params = {
|
|
177
|
+
"hashes": [image_hash]
|
|
178
|
+
}
|
|
179
|
+
image_data = await make_api_request(image_endpoint, access_token, image_params)
|
|
180
|
+
if "data" in image_data and len(image_data["data"]) > 0:
|
|
181
|
+
creative_data["full_image_url"] = image_data["data"][0].get("url")
|
|
182
|
+
|
|
183
|
+
# 3. If there's an asset_feed_spec, try to get images from there
|
|
184
|
+
if "asset_feed_spec" in creative_data and "images" in creative_data["asset_feed_spec"]:
|
|
185
|
+
images = creative_data["asset_feed_spec"]["images"]
|
|
186
|
+
if images and len(images) > 0 and "hash" in images[0]:
|
|
187
|
+
image_hash = images[0]["hash"]
|
|
188
|
+
account_id = ad_data.get('account_id', '')
|
|
189
|
+
if not account_id:
|
|
190
|
+
# Try to get account ID
|
|
191
|
+
ad_details_endpoint = f"{ad_id}"
|
|
192
|
+
ad_details_params = {
|
|
193
|
+
"fields": "account_id"
|
|
194
|
+
}
|
|
195
|
+
ad_details = await make_api_request(ad_details_endpoint, access_token, ad_details_params)
|
|
196
|
+
account_id = ad_details.get('account_id', '')
|
|
197
|
+
|
|
198
|
+
if account_id:
|
|
199
|
+
image_endpoint = f"act_{account_id}/adimages"
|
|
200
|
+
image_params = {
|
|
201
|
+
"hashes": [image_hash]
|
|
202
|
+
}
|
|
203
|
+
image_data = await make_api_request(image_endpoint, access_token, image_params)
|
|
204
|
+
if "data" in image_data and len(image_data["data"]) > 0:
|
|
205
|
+
creative_data["full_image_url"] = image_data["data"][0].get("url")
|
|
206
|
+
|
|
207
|
+
# If we have a thumbnail_url but no full_image_url, let's attempt to convert the thumbnail URL to full size
|
|
208
|
+
if "thumbnail_url" in creative_data and "full_image_url" not in creative_data:
|
|
209
|
+
thumbnail_url = creative_data["thumbnail_url"]
|
|
210
|
+
# Try to convert the URL to get higher resolution by removing size parameters
|
|
211
|
+
if "p64x64" in thumbnail_url:
|
|
212
|
+
full_url = thumbnail_url.replace("p64x64", "p1080x1080")
|
|
213
|
+
creative_data["full_image_url"] = full_url
|
|
214
|
+
elif "dst-emg0" in thumbnail_url:
|
|
215
|
+
# Remove the dst-emg0 parameter that seems to reduce size
|
|
216
|
+
full_url = thumbnail_url.replace("dst-emg0_", "")
|
|
217
|
+
creative_data["full_image_url"] = full_url
|
|
218
|
+
|
|
219
|
+
# Fallback to using thumbnail or image_url if we still don't have a full image
|
|
220
|
+
if "full_image_url" not in creative_data:
|
|
221
|
+
if "thumbnail_url" in creative_data:
|
|
222
|
+
creative_data["full_image_url"] = creative_data["thumbnail_url"]
|
|
223
|
+
elif "image_url" in creative_data:
|
|
224
|
+
creative_data["full_image_url"] = creative_data["image_url"]
|
|
225
|
+
|
|
226
|
+
return json.dumps(creative_data, indent=2)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@meta_api_tool
|
|
230
|
+
async def get_ad_image(access_token: str = None, ad_id: str = None) -> Image:
|
|
231
|
+
"""
|
|
232
|
+
Get, download, and visualize a Meta ad image in one step. Useful to see the image in the LLM.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
236
|
+
ad_id: Meta Ads ad ID
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
The ad image ready for direct visual analysis
|
|
240
|
+
"""
|
|
241
|
+
if not ad_id:
|
|
242
|
+
return "Error: No ad ID provided"
|
|
243
|
+
|
|
244
|
+
print(f"Attempting to get and analyze creative image for ad {ad_id}")
|
|
245
|
+
|
|
246
|
+
# First, get creative and account IDs
|
|
247
|
+
ad_endpoint = f"{ad_id}"
|
|
248
|
+
ad_params = {
|
|
249
|
+
"fields": "creative{id},account_id"
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
ad_data = await make_api_request(ad_endpoint, access_token, ad_params)
|
|
253
|
+
|
|
254
|
+
if "error" in ad_data:
|
|
255
|
+
return f"Error: Could not get ad data - {json.dumps(ad_data)}"
|
|
256
|
+
|
|
257
|
+
# Extract account_id
|
|
258
|
+
account_id = ad_data.get("account_id", "")
|
|
259
|
+
if not account_id:
|
|
260
|
+
return "Error: No account ID found"
|
|
261
|
+
|
|
262
|
+
# Extract creative ID
|
|
263
|
+
if "creative" not in ad_data:
|
|
264
|
+
return "Error: No creative found for this ad"
|
|
265
|
+
|
|
266
|
+
creative_data = ad_data.get("creative", {})
|
|
267
|
+
creative_id = creative_data.get("id")
|
|
268
|
+
if not creative_id:
|
|
269
|
+
return "Error: No creative ID found"
|
|
270
|
+
|
|
271
|
+
# Get creative details to find image hash
|
|
272
|
+
creative_endpoint = f"{creative_id}"
|
|
273
|
+
creative_params = {
|
|
274
|
+
"fields": "id,name,image_hash,asset_feed_spec"
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
creative_details = await make_api_request(creative_endpoint, access_token, creative_params)
|
|
278
|
+
|
|
279
|
+
# Identify image hashes to use from creative
|
|
280
|
+
image_hashes = []
|
|
281
|
+
|
|
282
|
+
# Check for direct image_hash on creative
|
|
283
|
+
if "image_hash" in creative_details:
|
|
284
|
+
image_hashes.append(creative_details["image_hash"])
|
|
285
|
+
|
|
286
|
+
# Check asset_feed_spec for image hashes - common in Advantage+ ads
|
|
287
|
+
if "asset_feed_spec" in creative_details and "images" in creative_details["asset_feed_spec"]:
|
|
288
|
+
for image in creative_details["asset_feed_spec"]["images"]:
|
|
289
|
+
if "hash" in image:
|
|
290
|
+
image_hashes.append(image["hash"])
|
|
291
|
+
|
|
292
|
+
if not image_hashes:
|
|
293
|
+
# If no hashes found, try to extract from the first creative we found in the API
|
|
294
|
+
# Get creative for ad to try to extract hash
|
|
295
|
+
creative_json = await get_ad_creatives(ad_id, "", access_token)
|
|
296
|
+
creative_data = json.loads(creative_json)
|
|
297
|
+
|
|
298
|
+
# Try to extract hash from asset_feed_spec
|
|
299
|
+
if "asset_feed_spec" in creative_data and "images" in creative_data["asset_feed_spec"]:
|
|
300
|
+
images = creative_data["asset_feed_spec"]["images"]
|
|
301
|
+
if images and len(images) > 0 and "hash" in images[0]:
|
|
302
|
+
image_hashes.append(images[0]["hash"])
|
|
303
|
+
|
|
304
|
+
if not image_hashes:
|
|
305
|
+
return "Error: No image hashes found in creative"
|
|
306
|
+
|
|
307
|
+
print(f"Found image hashes: {image_hashes}")
|
|
308
|
+
|
|
309
|
+
# Now fetch image data using adimages endpoint with specific format
|
|
310
|
+
image_endpoint = f"act_{account_id}/adimages"
|
|
311
|
+
|
|
312
|
+
# Format the hashes parameter exactly as in our successful curl test
|
|
313
|
+
hashes_str = f'["{image_hashes[0]}"]' # Format first hash only, as JSON string array
|
|
314
|
+
|
|
315
|
+
image_params = {
|
|
316
|
+
"fields": "hash,url,width,height,name,status",
|
|
317
|
+
"hashes": hashes_str
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
print(f"Requesting image data with params: {image_params}")
|
|
321
|
+
image_data = await make_api_request(image_endpoint, access_token, image_params)
|
|
322
|
+
|
|
323
|
+
if "error" in image_data:
|
|
324
|
+
return f"Error: Failed to get image data - {json.dumps(image_data)}"
|
|
325
|
+
|
|
326
|
+
if "data" not in image_data or not image_data["data"]:
|
|
327
|
+
return "Error: No image data returned from API"
|
|
328
|
+
|
|
329
|
+
# Get the first image URL
|
|
330
|
+
first_image = image_data["data"][0]
|
|
331
|
+
image_url = first_image.get("url")
|
|
332
|
+
|
|
333
|
+
if not image_url:
|
|
334
|
+
return "Error: No valid image URL found"
|
|
335
|
+
|
|
336
|
+
print(f"Downloading image from URL: {image_url}")
|
|
337
|
+
|
|
338
|
+
# Download the image
|
|
339
|
+
image_bytes = await download_image(image_url)
|
|
340
|
+
|
|
341
|
+
if not image_bytes:
|
|
342
|
+
return "Error: Failed to download image"
|
|
343
|
+
|
|
344
|
+
try:
|
|
345
|
+
# Convert bytes to PIL Image
|
|
346
|
+
img = PILImage.open(io.BytesIO(image_bytes))
|
|
347
|
+
|
|
348
|
+
# Convert to RGB if needed
|
|
349
|
+
if img.mode != "RGB":
|
|
350
|
+
img = img.convert("RGB")
|
|
351
|
+
|
|
352
|
+
# Create a byte stream of the image data
|
|
353
|
+
byte_arr = io.BytesIO()
|
|
354
|
+
img.save(byte_arr, format="JPEG")
|
|
355
|
+
img_bytes = byte_arr.getvalue()
|
|
356
|
+
|
|
357
|
+
# Return as an Image object that LLM can directly analyze
|
|
358
|
+
return Image(data=img_bytes, format="jpeg")
|
|
359
|
+
|
|
360
|
+
except Exception as e:
|
|
361
|
+
return f"Error processing image: {str(e)}"
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Ad Set-related functionality for Meta Ads API."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Optional, Dict, Any, List
|
|
5
|
+
from .api import meta_api_tool, make_api_request
|
|
6
|
+
from .accounts import get_ad_accounts
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@meta_api_tool
|
|
10
|
+
async def get_adsets(access_token: str = None, account_id: str = None, limit: int = 10, campaign_id: str = "") -> str:
|
|
11
|
+
"""
|
|
12
|
+
Get ad sets for a Meta Ads account with optional filtering by campaign.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
16
|
+
account_id: Meta Ads account ID (format: act_XXXXXXXXX)
|
|
17
|
+
limit: Maximum number of ad sets to return (default: 10)
|
|
18
|
+
campaign_id: Optional campaign ID to filter by
|
|
19
|
+
"""
|
|
20
|
+
# If no account ID is specified, try to get the first one for the user
|
|
21
|
+
if not account_id:
|
|
22
|
+
accounts_json = await get_ad_accounts("me", json.dumps({"limit": 1}), access_token)
|
|
23
|
+
accounts_data = json.loads(accounts_json)
|
|
24
|
+
|
|
25
|
+
if "data" in accounts_data and accounts_data["data"]:
|
|
26
|
+
account_id = accounts_data["data"][0]["id"]
|
|
27
|
+
else:
|
|
28
|
+
return json.dumps({"error": "No account ID specified and no accounts found for user"}, indent=2)
|
|
29
|
+
|
|
30
|
+
endpoint = f"{account_id}/adsets"
|
|
31
|
+
params = {
|
|
32
|
+
"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",
|
|
33
|
+
"limit": limit
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if campaign_id:
|
|
37
|
+
params["campaign_id"] = campaign_id
|
|
38
|
+
|
|
39
|
+
data = await make_api_request(endpoint, access_token, params)
|
|
40
|
+
|
|
41
|
+
return json.dumps(data, indent=2)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@meta_api_tool
|
|
45
|
+
async def get_adset_details(access_token: str = None, adset_id: str = None) -> str:
|
|
46
|
+
"""
|
|
47
|
+
Get detailed information about a specific ad set.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
adset_id: Meta Ads ad set ID (required)
|
|
51
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
52
|
+
|
|
53
|
+
Example:
|
|
54
|
+
To call this function through MCP, pass the adset_id as the first argument:
|
|
55
|
+
{
|
|
56
|
+
"args": "YOUR_ADSET_ID"
|
|
57
|
+
}
|
|
58
|
+
"""
|
|
59
|
+
if not adset_id:
|
|
60
|
+
return json.dumps({"error": "No ad set ID provided"}, indent=2)
|
|
61
|
+
|
|
62
|
+
endpoint = f"{adset_id}"
|
|
63
|
+
params = {
|
|
64
|
+
"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"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
data = await make_api_request(endpoint, access_token, params)
|
|
68
|
+
|
|
69
|
+
return json.dumps(data, indent=2)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@meta_api_tool
|
|
73
|
+
async def update_adset(access_token: str = None, adset_id: str = None,
|
|
74
|
+
bid_strategy: Optional[str] = None,
|
|
75
|
+
bid_amount: Optional[int] = None,
|
|
76
|
+
frequency_control_specs: Optional[List[Dict[str, Any]]] = None,
|
|
77
|
+
status: Optional[str] = None) -> str:
|
|
78
|
+
"""
|
|
79
|
+
Update an existing ad set with new settings including frequency caps.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
83
|
+
adset_id: Meta Ads ad set ID
|
|
84
|
+
bid_strategy: Bid strategy (e.g., 'LOWEST_COST_WITH_BID_CAP')
|
|
85
|
+
bid_amount: Bid amount in your account's currency (in cents for USD)
|
|
86
|
+
frequency_control_specs: List of frequency control specifications. Each spec should have:
|
|
87
|
+
- event: Type of event (e.g., 'IMPRESSIONS')
|
|
88
|
+
- interval_days: Number of days for the frequency cap
|
|
89
|
+
- max_frequency: Maximum number of times to show the ad
|
|
90
|
+
status: Update ad set status (ACTIVE, PAUSED, etc.)
|
|
91
|
+
"""
|
|
92
|
+
if not adset_id:
|
|
93
|
+
return json.dumps({"error": "No ad set ID provided"}, indent=2)
|
|
94
|
+
|
|
95
|
+
endpoint = f"{adset_id}"
|
|
96
|
+
params = {}
|
|
97
|
+
|
|
98
|
+
if bid_strategy:
|
|
99
|
+
params["bid_strategy"] = bid_strategy
|
|
100
|
+
|
|
101
|
+
if bid_amount is not None:
|
|
102
|
+
params["bid_amount"] = bid_amount
|
|
103
|
+
|
|
104
|
+
if frequency_control_specs:
|
|
105
|
+
params["frequency_control_specs"] = frequency_control_specs
|
|
106
|
+
|
|
107
|
+
if status:
|
|
108
|
+
params["status"] = status
|
|
109
|
+
|
|
110
|
+
if not params:
|
|
111
|
+
return json.dumps({"error": "No update parameters provided"}, indent=2)
|
|
112
|
+
|
|
113
|
+
data = await make_api_request(endpoint, access_token, params, method="POST")
|
|
114
|
+
|
|
115
|
+
return json.dumps(data, indent=2)
|