meta-ads-mcp 0.10.9__tar.gz → 0.11.1__tar.gz
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-0.10.9 → meta_ads_mcp-0.11.1}/PKG-INFO +1 -1
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/__init__.py +1 -1
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/accounts.py +4 -4
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/ads.py +176 -177
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/ads_library.py +5 -5
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/adsets.py +28 -34
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/api.py +1 -1
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/auth.py +1 -1
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/authentication.py +2 -1
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/budget_schedules.py +1 -1
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/campaigns.py +33 -39
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/duplication.py +4 -4
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/insights.py +2 -2
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/openai_deep_research.py +4 -4
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/pipeboard_auth.py +1 -1
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/reports.py +3 -3
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/targeting.py +14 -14
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/pyproject.toml +1 -1
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_dsa_beneficiary.py +1 -1
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_dsa_integration.py +1 -1
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_dynamic_creatives.py +2 -1
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_insights_actions_and_values.py +7 -10
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_targeting.py +15 -6
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/.github/workflows/publish.yml +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/.github/workflows/test.yml +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/.gitignore +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/CUSTOM_META_APP.md +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/Dockerfile +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/LICENSE +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/LOCAL_INSTALLATION.md +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/META_API_NOTES.md +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/README.md +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/RELEASE.md +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/STREAMABLE_HTTP_SETUP.md +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/examples/README.md +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/examples/example_http_client.py +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/future_improvements.md +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/images/meta-ads-example.png +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_auth.sh +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/__main__.py +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/__init__.py +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/callback_server.py +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/http_auth_integration.py +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/resources.py +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/server.py +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/utils.py +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/requirements.txt +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/setup.py +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/smithery.yaml +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/README.md +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/README_REGRESSION_TESTS.md +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/__init__.py +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/conftest.py +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/e2e_account_info_search_issue.py +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_account_info_access_fix.py +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_account_search.py +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_budget_update.py +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_budget_update_e2e.py +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_duplication.py +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_duplication_regression.py +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_estimate_audience_size.py +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_estimate_audience_size_e2e.py +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_get_account_pages.py +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_get_ad_creatives_fix.py +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_get_ad_image_quality_improvements.py +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_get_ad_image_regression.py +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_http_transport.py +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_integration_openai_mcp.py +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_mobile_app_adset_creation.py +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_mobile_app_adset_issue.py +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_openai.py +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_openai_mcp_deep_research.py +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_page_discovery.py +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_page_discovery_integration.py +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_targeting_search_e2e.py +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_update_ad_creative_id.py +0 -0
- {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_upload_ad_image.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meta-ads-mcp
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.1
|
|
4
4
|
Summary: Model Context Protocol (MCP) server for interacting with Meta Ads API
|
|
5
5
|
Project-URL: Homepage, https://github.com/pipeboard-co/meta-ads-mcp
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/pipeboard-co/meta-ads-mcp/issues
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
"""Account-related functionality for Meta Ads API."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
-
from typing import Optional
|
|
4
|
+
from typing import Optional, Dict, Any
|
|
5
5
|
from .api import meta_api_tool, make_api_request
|
|
6
6
|
from .server import mcp_server
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
@mcp_server.tool()
|
|
10
10
|
@meta_api_tool
|
|
11
|
-
async def get_ad_accounts(access_token: str = None, user_id: str = "me", limit: int = 200) -> str:
|
|
11
|
+
async def get_ad_accounts(access_token: Optional[str] = None, user_id: str = "me", limit: int = 200) -> str:
|
|
12
12
|
"""
|
|
13
13
|
Get ad accounts accessible by a user.
|
|
14
14
|
|
|
@@ -30,13 +30,13 @@ async def get_ad_accounts(access_token: str = None, user_id: str = "me", limit:
|
|
|
30
30
|
|
|
31
31
|
@mcp_server.tool()
|
|
32
32
|
@meta_api_tool
|
|
33
|
-
async def get_account_info(
|
|
33
|
+
async def get_account_info(account_id: str, access_token: Optional[str] = None) -> str:
|
|
34
34
|
"""
|
|
35
35
|
Get detailed information about a specific ad account.
|
|
36
36
|
|
|
37
37
|
Args:
|
|
38
|
+
account_id: Meta Ads account ID (format: act_XXXXXXXXX)
|
|
38
39
|
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
39
|
-
account_id: Meta Ads account ID (format: act_XXXXXXXXX) - REQUIRED
|
|
40
40
|
"""
|
|
41
41
|
if not account_id:
|
|
42
42
|
return {
|
|
@@ -14,29 +14,27 @@ from .utils import download_image, try_multiple_download_methods, ad_creative_im
|
|
|
14
14
|
from .server import mcp_server
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
# Only register the save_ad_image_locally function if explicitly enabled via environment variable
|
|
18
|
+
ENABLE_SAVE_AD_IMAGE_LOCALLY = bool(os.environ.get("META_ADS_ENABLE_SAVE_AD_IMAGE_LOCALLY", ""))
|
|
19
|
+
|
|
20
|
+
|
|
17
21
|
@mcp_server.tool()
|
|
18
22
|
@meta_api_tool
|
|
19
|
-
async def get_ads(
|
|
23
|
+
async def get_ads(account_id: str, access_token: Optional[str] = None, limit: int = 10,
|
|
20
24
|
campaign_id: str = "", adset_id: str = "") -> str:
|
|
21
25
|
"""
|
|
22
26
|
Get ads for a Meta Ads account with optional filtering.
|
|
23
27
|
|
|
24
28
|
Args:
|
|
25
|
-
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
26
29
|
account_id: Meta Ads account ID (format: act_XXXXXXXXX)
|
|
30
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
27
31
|
limit: Maximum number of ads to return (default: 10)
|
|
28
32
|
campaign_id: Optional campaign ID to filter by
|
|
29
33
|
adset_id: Optional ad set ID to filter by
|
|
30
34
|
"""
|
|
31
|
-
#
|
|
35
|
+
# Require explicit account_id
|
|
32
36
|
if not account_id:
|
|
33
|
-
|
|
34
|
-
accounts_data = json.loads(accounts_json)
|
|
35
|
-
|
|
36
|
-
if "data" in accounts_data and accounts_data["data"]:
|
|
37
|
-
account_id = accounts_data["data"][0]["id"]
|
|
38
|
-
else:
|
|
39
|
-
return json.dumps({"error": "No account ID specified and no accounts found for user"}, indent=2)
|
|
37
|
+
return json.dumps({"error": "No account ID specified"}, indent=2)
|
|
40
38
|
|
|
41
39
|
# Prioritize adset_id over campaign_id - use adset-specific endpoint
|
|
42
40
|
if adset_id:
|
|
@@ -67,13 +65,13 @@ async def get_ads(access_token: str = None, account_id: str = None, limit: int =
|
|
|
67
65
|
|
|
68
66
|
@mcp_server.tool()
|
|
69
67
|
@meta_api_tool
|
|
70
|
-
async def get_ad_details(
|
|
68
|
+
async def get_ad_details(ad_id: str, access_token: Optional[str] = None) -> str:
|
|
71
69
|
"""
|
|
72
70
|
Get detailed information about a specific ad.
|
|
73
71
|
|
|
74
72
|
Args:
|
|
75
|
-
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
76
73
|
ad_id: Meta Ads ad ID
|
|
74
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
77
75
|
"""
|
|
78
76
|
if not ad_id:
|
|
79
77
|
return json.dumps({"error": "No ad ID provided"}, indent=2)
|
|
@@ -91,14 +89,14 @@ async def get_ad_details(access_token: str = None, ad_id: str = None) -> str:
|
|
|
91
89
|
@mcp_server.tool()
|
|
92
90
|
@meta_api_tool
|
|
93
91
|
async def create_ad(
|
|
94
|
-
account_id: str
|
|
95
|
-
name: str
|
|
96
|
-
adset_id: str
|
|
97
|
-
creative_id: str
|
|
92
|
+
account_id: str,
|
|
93
|
+
name: str,
|
|
94
|
+
adset_id: str,
|
|
95
|
+
creative_id: str,
|
|
98
96
|
status: str = "PAUSED",
|
|
99
|
-
bid_amount = None,
|
|
97
|
+
bid_amount: Optional[int] = None,
|
|
100
98
|
tracking_specs: Optional[List[Dict[str, Any]]] = None,
|
|
101
|
-
access_token: str = None
|
|
99
|
+
access_token: Optional[str] = None
|
|
102
100
|
) -> str:
|
|
103
101
|
"""
|
|
104
102
|
Create a new ad with an existing creative.
|
|
@@ -158,13 +156,13 @@ async def create_ad(
|
|
|
158
156
|
|
|
159
157
|
@mcp_server.tool()
|
|
160
158
|
@meta_api_tool
|
|
161
|
-
async def get_ad_creatives(
|
|
159
|
+
async def get_ad_creatives(ad_id: str, access_token: Optional[str] = None) -> str:
|
|
162
160
|
"""
|
|
163
161
|
Get creative details for a specific ad. Best if combined with get_ad_image to get the full image.
|
|
164
162
|
|
|
165
163
|
Args:
|
|
166
|
-
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
167
164
|
ad_id: Meta Ads ad ID
|
|
165
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
168
166
|
"""
|
|
169
167
|
if not ad_id:
|
|
170
168
|
return json.dumps({"error": "No ad ID provided"}, indent=2)
|
|
@@ -186,13 +184,13 @@ async def get_ad_creatives(access_token: str = None, ad_id: str = None) -> str:
|
|
|
186
184
|
|
|
187
185
|
@mcp_server.tool()
|
|
188
186
|
@meta_api_tool
|
|
189
|
-
async def get_ad_image(
|
|
187
|
+
async def get_ad_image(ad_id: str, access_token: Optional[str] = None) -> Image:
|
|
190
188
|
"""
|
|
191
189
|
Get, download, and visualize a Meta ad image in one step. Useful to see the image in the LLM.
|
|
192
190
|
|
|
193
191
|
Args:
|
|
194
|
-
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
195
192
|
ad_id: Meta Ads ad ID
|
|
193
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
196
194
|
|
|
197
195
|
Returns:
|
|
198
196
|
The ad image ready for direct visual analysis
|
|
@@ -386,141 +384,142 @@ async def get_ad_image(access_token: str = None, ad_id: str = None) -> Image:
|
|
|
386
384
|
return f"Error processing image: {str(e)}"
|
|
387
385
|
|
|
388
386
|
|
|
389
|
-
|
|
390
|
-
@
|
|
391
|
-
|
|
392
|
-
""
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
Args:
|
|
396
|
-
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
397
|
-
ad_id: Meta Ads ad ID
|
|
398
|
-
output_dir: Directory to save the image file (default: 'ad_images')
|
|
399
|
-
|
|
400
|
-
Returns:
|
|
401
|
-
The file path to the saved image, or an error message string.
|
|
402
|
-
"""
|
|
403
|
-
if not ad_id:
|
|
404
|
-
return json.dumps({"error": "No ad ID provided"}, indent=2)
|
|
387
|
+
if ENABLE_SAVE_AD_IMAGE_LOCALLY:
|
|
388
|
+
@mcp_server.tool()
|
|
389
|
+
@meta_api_tool
|
|
390
|
+
async def save_ad_image_locally(ad_id: str, access_token: Optional[str] = None, output_dir: str = "ad_images") -> str:
|
|
391
|
+
"""
|
|
392
|
+
Get, download, and save a Meta ad image locally, returning the file path.
|
|
405
393
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
ad_params = {
|
|
411
|
-
"fields": "creative{id},account_id"
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
ad_data = await make_api_request(ad_endpoint, access_token, ad_params)
|
|
415
|
-
|
|
416
|
-
if "error" in ad_data:
|
|
417
|
-
return json.dumps({"error": f"Could not get ad data - {json.dumps(ad_data)}"}, indent=2)
|
|
418
|
-
|
|
419
|
-
account_id = ad_data.get("account_id")
|
|
420
|
-
if not account_id:
|
|
421
|
-
return json.dumps({"error": "No account ID found for ad"}, indent=2)
|
|
422
|
-
|
|
423
|
-
if "creative" not in ad_data:
|
|
424
|
-
return json.dumps({"error": "No creative found for this ad"}, indent=2)
|
|
394
|
+
Args:
|
|
395
|
+
ad_id: Meta Ads ad ID
|
|
396
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
397
|
+
output_dir: Directory to save the image file (default: 'ad_images')
|
|
425
398
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
399
|
+
Returns:
|
|
400
|
+
The file path to the saved image, or an error message string.
|
|
401
|
+
"""
|
|
402
|
+
if not ad_id:
|
|
403
|
+
return json.dumps({"error": "No ad ID provided"}, indent=2)
|
|
404
|
+
|
|
405
|
+
print(f"Attempting to get and save creative image for ad {ad_id}")
|
|
406
|
+
|
|
407
|
+
# First, get creative and account IDs
|
|
408
|
+
ad_endpoint = f"{ad_id}"
|
|
409
|
+
ad_params = {
|
|
410
|
+
"fields": "creative{id},account_id"
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
ad_data = await make_api_request(ad_endpoint, access_token, ad_params)
|
|
414
|
+
|
|
415
|
+
if "error" in ad_data:
|
|
416
|
+
return json.dumps({"error": f"Could not get ad data - {json.dumps(ad_data)}"}, indent=2)
|
|
417
|
+
|
|
418
|
+
account_id = ad_data.get("account_id")
|
|
419
|
+
if not account_id:
|
|
420
|
+
return json.dumps({"error": "No account ID found for ad"}, indent=2)
|
|
421
|
+
|
|
422
|
+
if "creative" not in ad_data:
|
|
423
|
+
return json.dumps({"error": "No creative found for this ad"}, indent=2)
|
|
424
|
+
|
|
425
|
+
creative_data = ad_data.get("creative", {})
|
|
426
|
+
creative_id = creative_data.get("id")
|
|
427
|
+
if not creative_id:
|
|
428
|
+
return json.dumps({"error": "No creative ID found"}, indent=2)
|
|
429
|
+
|
|
430
|
+
# Get creative details to find image hash
|
|
431
|
+
creative_endpoint = f"{creative_id}"
|
|
432
|
+
creative_params = {
|
|
433
|
+
"fields": "id,name,image_hash,asset_feed_spec"
|
|
434
|
+
}
|
|
435
|
+
creative_details = await make_api_request(creative_endpoint, access_token, creative_params)
|
|
436
|
+
|
|
437
|
+
image_hashes = []
|
|
438
|
+
if "image_hash" in creative_details:
|
|
439
|
+
image_hashes.append(creative_details["image_hash"])
|
|
440
|
+
if "asset_feed_spec" in creative_details and "images" in creative_details["asset_feed_spec"]:
|
|
441
|
+
for image in creative_details["asset_feed_spec"]["images"]:
|
|
442
|
+
if "hash" in image:
|
|
443
|
+
image_hashes.append(image["hash"])
|
|
444
|
+
|
|
445
|
+
if not image_hashes:
|
|
446
|
+
# Fallback attempt (as in get_ad_image)
|
|
447
|
+
creative_json = await get_ad_creatives(ad_id=ad_id, access_token=access_token) # Ensure ad_id is passed correctly
|
|
448
|
+
creative_data_list = json.loads(creative_json)
|
|
449
|
+
if 'data' in creative_data_list and creative_data_list['data']:
|
|
450
|
+
first_creative = creative_data_list['data'][0]
|
|
451
|
+
if 'object_story_spec' in first_creative and 'link_data' in first_creative['object_story_spec'] and 'image_hash' in first_creative['object_story_spec']['link_data']:
|
|
452
|
+
image_hashes.append(first_creative['object_story_spec']['link_data']['image_hash'])
|
|
453
|
+
elif 'image_hash' in first_creative: # Check direct hash on creative data
|
|
454
|
+
image_hashes.append(first_creative['image_hash'])
|
|
456
455
|
|
|
457
456
|
|
|
458
|
-
|
|
459
|
-
|
|
457
|
+
if not image_hashes:
|
|
458
|
+
return json.dumps({"error": "No image hashes found in creative or fallback"}, indent=2)
|
|
460
459
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
# Fetch image data using the first hash
|
|
464
|
-
image_endpoint = f"act_{account_id}/adimages"
|
|
465
|
-
hashes_str = f'["{image_hashes[0]}"]'
|
|
466
|
-
image_params = {
|
|
467
|
-
"fields": "hash,url,width,height,name,status",
|
|
468
|
-
"hashes": hashes_str
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
print(f"Requesting image data with params: {image_params}")
|
|
472
|
-
image_data = await make_api_request(image_endpoint, access_token, image_params)
|
|
473
|
-
|
|
474
|
-
if "error" in image_data:
|
|
475
|
-
return json.dumps({"error": f"Failed to get image data - {json.dumps(image_data)}"}, indent=2)
|
|
476
|
-
|
|
477
|
-
if "data" not in image_data or not image_data["data"]:
|
|
478
|
-
return json.dumps({"error": "No image data returned from API"}, indent=2)
|
|
460
|
+
print(f"Found image hashes: {image_hashes}")
|
|
479
461
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
462
|
+
# Fetch image data using the first hash
|
|
463
|
+
image_endpoint = f"act_{account_id}/adimages"
|
|
464
|
+
hashes_str = f'["{image_hashes[0]}"]'
|
|
465
|
+
image_params = {
|
|
466
|
+
"fields": "hash,url,width,height,name,status",
|
|
467
|
+
"hashes": hashes_str
|
|
468
|
+
}
|
|
485
469
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
# Download and Save Image
|
|
489
|
-
image_bytes = await download_image(image_url)
|
|
490
|
-
|
|
491
|
-
if not image_bytes:
|
|
492
|
-
return json.dumps({"error": "Failed to download image"}, indent=2)
|
|
470
|
+
print(f"Requesting image data with params: {image_params}")
|
|
471
|
+
image_data = await make_api_request(image_endpoint, access_token, image_params)
|
|
493
472
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
473
|
+
if "error" in image_data:
|
|
474
|
+
return json.dumps({"error": f"Failed to get image data - {json.dumps(image_data)}"}, indent=2)
|
|
475
|
+
|
|
476
|
+
if "data" not in image_data or not image_data["data"]:
|
|
477
|
+
return json.dumps({"error": "No image data returned from API"}, indent=2)
|
|
498
478
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
# Save the image bytes to the file
|
|
505
|
-
with open(filepath, "wb") as f:
|
|
506
|
-
f.write(image_bytes)
|
|
479
|
+
first_image = image_data["data"][0]
|
|
480
|
+
image_url = first_image.get("url")
|
|
481
|
+
|
|
482
|
+
if not image_url:
|
|
483
|
+
return json.dumps({"error": "No valid image URL found in API response"}, indent=2)
|
|
507
484
|
|
|
508
|
-
print(f"
|
|
509
|
-
|
|
485
|
+
print(f"Downloading image from URL: {image_url}")
|
|
486
|
+
|
|
487
|
+
# Download and Save Image
|
|
488
|
+
image_bytes = await download_image(image_url)
|
|
489
|
+
|
|
490
|
+
if not image_bytes:
|
|
491
|
+
return json.dumps({"error": "Failed to download image"}, indent=2)
|
|
492
|
+
|
|
493
|
+
try:
|
|
494
|
+
# Ensure output directory exists
|
|
495
|
+
if not os.path.exists(output_dir):
|
|
496
|
+
os.makedirs(output_dir)
|
|
497
|
+
|
|
498
|
+
# Create a filename (e.g., using ad_id and image hash)
|
|
499
|
+
file_extension = ".jpg" # Default extension, could try to infer from headers later
|
|
500
|
+
filename = f"{ad_id}_{image_hashes[0]}{file_extension}"
|
|
501
|
+
filepath = os.path.join(output_dir, filename)
|
|
502
|
+
|
|
503
|
+
# Save the image bytes to the file
|
|
504
|
+
with open(filepath, "wb") as f:
|
|
505
|
+
f.write(image_bytes)
|
|
506
|
+
|
|
507
|
+
print(f"Image saved successfully to: {filepath}")
|
|
508
|
+
return json.dumps({"filepath": filepath}, indent=2) # Return JSON with filepath
|
|
510
509
|
|
|
511
|
-
|
|
512
|
-
|
|
510
|
+
except Exception as e:
|
|
511
|
+
return json.dumps({"error": f"Failed to save image: {str(e)}"}, indent=2)
|
|
513
512
|
|
|
514
513
|
|
|
515
514
|
@mcp_server.tool()
|
|
516
515
|
@meta_api_tool
|
|
517
516
|
async def update_ad(
|
|
518
517
|
ad_id: str,
|
|
519
|
-
status: str = None,
|
|
520
|
-
bid_amount: int = None,
|
|
521
|
-
tracking_specs = None,
|
|
522
|
-
creative_id: str = None,
|
|
523
|
-
access_token: str = None
|
|
518
|
+
status: Optional[str] = None,
|
|
519
|
+
bid_amount: Optional[int] = None,
|
|
520
|
+
tracking_specs: Optional[List[Dict[str, Any]]] = None,
|
|
521
|
+
creative_id: Optional[str] = None,
|
|
522
|
+
access_token: Optional[str] = None
|
|
524
523
|
) -> str:
|
|
525
524
|
"""
|
|
526
525
|
Update an ad with new settings.
|
|
@@ -562,18 +561,18 @@ async def update_ad(
|
|
|
562
561
|
@mcp_server.tool()
|
|
563
562
|
@meta_api_tool
|
|
564
563
|
async def upload_ad_image(
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
file: str = None,
|
|
568
|
-
image_url: str = None,
|
|
569
|
-
name: str = None
|
|
564
|
+
account_id: str,
|
|
565
|
+
access_token: Optional[str] = None,
|
|
566
|
+
file: Optional[str] = None,
|
|
567
|
+
image_url: Optional[str] = None,
|
|
568
|
+
name: Optional[str] = None
|
|
570
569
|
) -> str:
|
|
571
570
|
"""
|
|
572
571
|
Upload an image to use in Meta Ads creatives.
|
|
573
572
|
|
|
574
573
|
Args:
|
|
575
|
-
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
576
574
|
account_id: Meta Ads account ID (format: act_XXXXXXXXX)
|
|
575
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
577
576
|
file: Data URL or raw base64 string of the image (e.g., "data:image/png;base64,iVBORw0KG...")
|
|
578
577
|
image_url: Direct URL to an image to fetch and upload
|
|
579
578
|
name: Optional name for the image (default: filename)
|
|
@@ -731,29 +730,29 @@ async def upload_ad_image(
|
|
|
731
730
|
@mcp_server.tool()
|
|
732
731
|
@meta_api_tool
|
|
733
732
|
async def create_ad_creative(
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
page_id: str = None,
|
|
739
|
-
link_url: str = None,
|
|
740
|
-
message: str = None,
|
|
741
|
-
headline: str = None,
|
|
742
|
-
headlines: List[str] = None,
|
|
743
|
-
description: str = None,
|
|
744
|
-
descriptions: List[str] = None,
|
|
745
|
-
dynamic_creative_spec: Dict[str, Any] = None,
|
|
746
|
-
call_to_action_type: str = None,
|
|
747
|
-
instagram_actor_id: str = None
|
|
733
|
+
account_id: str,
|
|
734
|
+
image_hash: str,
|
|
735
|
+
access_token: Optional[str] = None,
|
|
736
|
+
name: Optional[str] = None,
|
|
737
|
+
page_id: Optional[str] = None,
|
|
738
|
+
link_url: Optional[str] = None,
|
|
739
|
+
message: Optional[str] = None,
|
|
740
|
+
headline: Optional[str] = None,
|
|
741
|
+
headlines: Optional[List[str]] = None,
|
|
742
|
+
description: Optional[str] = None,
|
|
743
|
+
descriptions: Optional[List[str]] = None,
|
|
744
|
+
dynamic_creative_spec: Optional[Dict[str, Any]] = None,
|
|
745
|
+
call_to_action_type: Optional[str] = None,
|
|
746
|
+
instagram_actor_id: Optional[str] = None
|
|
748
747
|
) -> str:
|
|
749
748
|
"""
|
|
750
749
|
Create a new ad creative using an uploaded image hash.
|
|
751
750
|
|
|
752
751
|
Args:
|
|
753
|
-
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
754
752
|
account_id: Meta Ads account ID (format: act_XXXXXXXXX)
|
|
755
|
-
name: Creative name
|
|
756
753
|
image_hash: Hash of the uploaded image
|
|
754
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
755
|
+
name: Creative name
|
|
757
756
|
page_id: Facebook Page ID to be used for the ad
|
|
758
757
|
link_url: Destination URL for the ad
|
|
759
758
|
message: Ad copy/text
|
|
@@ -943,23 +942,23 @@ async def create_ad_creative(
|
|
|
943
942
|
@mcp_server.tool()
|
|
944
943
|
@meta_api_tool
|
|
945
944
|
async def update_ad_creative(
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
name: str = None,
|
|
949
|
-
message: str = None,
|
|
950
|
-
headline: str = None,
|
|
951
|
-
headlines: List[str] = None,
|
|
952
|
-
description: str = None,
|
|
953
|
-
descriptions: List[str] = None,
|
|
954
|
-
dynamic_creative_spec: Dict[str, Any] = None,
|
|
955
|
-
call_to_action_type: str = None
|
|
945
|
+
creative_id: str,
|
|
946
|
+
access_token: Optional[str] = None,
|
|
947
|
+
name: Optional[str] = None,
|
|
948
|
+
message: Optional[str] = None,
|
|
949
|
+
headline: Optional[str] = None,
|
|
950
|
+
headlines: Optional[List[str]] = None,
|
|
951
|
+
description: Optional[str] = None,
|
|
952
|
+
descriptions: Optional[List[str]] = None,
|
|
953
|
+
dynamic_creative_spec: Optional[Dict[str, Any]] = None,
|
|
954
|
+
call_to_action_type: Optional[str] = None
|
|
956
955
|
) -> str:
|
|
957
956
|
"""
|
|
958
957
|
Update an existing ad creative with new content or settings.
|
|
959
958
|
|
|
960
959
|
Args:
|
|
961
|
-
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
962
960
|
creative_id: Meta Ads creative ID to update
|
|
961
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
963
962
|
name: New creative name
|
|
964
963
|
message: New ad copy/text
|
|
965
964
|
headline: Single headline for simple ads (cannot be used with headlines)
|
|
@@ -1252,13 +1251,13 @@ async def _search_pages_by_name_core(access_token: str, account_id: str, search_
|
|
|
1252
1251
|
|
|
1253
1252
|
@mcp_server.tool()
|
|
1254
1253
|
@meta_api_tool
|
|
1255
|
-
async def search_pages_by_name(
|
|
1254
|
+
async def search_pages_by_name(account_id: str, access_token: Optional[str] = None, search_term: Optional[str] = None) -> str:
|
|
1256
1255
|
"""
|
|
1257
1256
|
Search for pages by name within an account.
|
|
1258
1257
|
|
|
1259
1258
|
Args:
|
|
1260
|
-
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
1261
1259
|
account_id: Meta Ads account ID (format: act_XXXXXXXXX)
|
|
1260
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
1262
1261
|
search_term: Search term to find pages by name (optional - returns all pages if not provided)
|
|
1263
1262
|
|
|
1264
1263
|
Returns:
|
|
@@ -1275,13 +1274,13 @@ async def search_pages_by_name(access_token: str = None, account_id: str = None,
|
|
|
1275
1274
|
|
|
1276
1275
|
@mcp_server.tool()
|
|
1277
1276
|
@meta_api_tool
|
|
1278
|
-
async def get_account_pages(
|
|
1277
|
+
async def get_account_pages(account_id: str, access_token: Optional[str] = None) -> str:
|
|
1279
1278
|
"""
|
|
1280
1279
|
Get pages associated with a Meta Ads account.
|
|
1281
1280
|
|
|
1282
1281
|
Args:
|
|
1283
|
-
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
1284
1282
|
account_id: Meta Ads account ID (format: act_XXXXXXXXX)
|
|
1283
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
1285
1284
|
|
|
1286
1285
|
Returns:
|
|
1287
1286
|
JSON response with pages associated with the account
|
|
@@ -14,10 +14,10 @@ if not DISABLE_ADS_LIBRARY:
|
|
|
14
14
|
@mcp_server.tool()
|
|
15
15
|
@meta_api_tool
|
|
16
16
|
async def search_ads_archive(
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
search_terms: str,
|
|
18
|
+
ad_reached_countries: List[str],
|
|
19
|
+
access_token: Optional[str] = None,
|
|
19
20
|
ad_type: str = "ALL",
|
|
20
|
-
ad_reached_countries: List[str] = None,
|
|
21
21
|
limit: int = 25, # Default limit, adjust as needed
|
|
22
22
|
fields: str = "ad_creation_time,ad_creative_body,ad_creative_link_caption,ad_creative_link_description,ad_creative_link_title,ad_delivery_start_time,ad_delivery_stop_time,ad_snapshot_url,currency,demographic_distribution,funding_entity,impressions,page_id,page_name,publisher_platform,region_distribution,spend"
|
|
23
23
|
) -> str:
|
|
@@ -25,10 +25,10 @@ if not DISABLE_ADS_LIBRARY:
|
|
|
25
25
|
Search the Facebook Ads Library archive.
|
|
26
26
|
|
|
27
27
|
Args:
|
|
28
|
-
access_token: Meta API access token (optional - will use cached token if not provided).
|
|
29
28
|
search_terms: The search query for ads.
|
|
30
|
-
ad_type: Type of ads to search for (e.g., POLITICAL_AND_ISSUE_ADS, HOUSING_ADS, ALL).
|
|
31
29
|
ad_reached_countries: List of country codes (e.g., ["US", "GB"]).
|
|
30
|
+
access_token: Meta API access token (optional - will use cached token if not provided).
|
|
31
|
+
ad_type: Type of ads to search for (e.g., POLITICAL_AND_ISSUE_ADS, HOUSING_ADS, ALL).
|
|
32
32
|
limit: Maximum number of ads to return.
|
|
33
33
|
fields: Comma-separated string of fields to retrieve for each ad.
|
|
34
34
|
|