meta-ads-mcp 0.2.1__tar.gz → 0.2.2__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.2.1 → meta_ads_mcp-0.2.2}/.gitignore +10 -0
- {meta_ads_mcp-0.2.1 → meta_ads_mcp-0.2.2}/PKG-INFO +1 -1
- meta_ads_mcp-0.2.2/future_improvements.md +59 -0
- meta_ads_mcp-0.2.2/meta-ads-mcp +13 -0
- {meta_ads_mcp-0.2.1 → meta_ads_mcp-0.2.2}/meta_ads_mcp/__init__.py +19 -5
- meta_ads_mcp-0.2.2/meta_ads_mcp/core/__init__.py +34 -0
- meta_ads_mcp-0.2.2/meta_ads_mcp/core/accounts.py +59 -0
- meta_ads_mcp-0.2.2/meta_ads_mcp/core/ads.py +361 -0
- meta_ads_mcp-0.2.2/meta_ads_mcp/core/adsets.py +115 -0
- meta_ads_mcp-0.2.2/meta_ads_mcp/core/api.py +211 -0
- meta_ads_mcp-0.2.2/meta_ads_mcp/core/auth.py +416 -0
- meta_ads_mcp-0.2.2/meta_ads_mcp/core/authentication.py +56 -0
- meta_ads_mcp-0.2.2/meta_ads_mcp/core/campaigns.py +119 -0
- meta_ads_mcp-0.2.2/meta_ads_mcp/core/insights.py +412 -0
- meta_ads_mcp-0.2.2/meta_ads_mcp/core/resources.py +46 -0
- meta_ads_mcp-0.2.2/meta_ads_mcp/core/server.py +47 -0
- meta_ads_mcp-0.2.2/meta_ads_mcp/core/utils.py +128 -0
- {meta_ads_mcp-0.2.1 → meta_ads_mcp-0.2.2}/pyproject.toml +1 -1
- {meta_ads_mcp-0.2.1 → meta_ads_mcp-0.2.2}/test_meta_ads_auth.py +2 -1
- meta_ads_mcp-0.2.1/.python-version +0 -1
- meta_ads_mcp-0.2.1/.uv.toml +0 -2
- meta_ads_mcp-0.2.1/meta_ads_generated.py +0 -1840
- meta_ads_mcp-0.2.1/poetry.lock +0 -437
- meta_ads_mcp-0.2.1/uv.lock +0 -462
- {meta_ads_mcp-0.2.1 → meta_ads_mcp-0.2.2}/README.md +0 -0
- {meta_ads_mcp-0.2.1 → meta_ads_mcp-0.2.2}/meta_ads_mcp/api.py +0 -0
- {meta_ads_mcp-0.2.1 → meta_ads_mcp-0.2.2}/requirements.txt +0 -0
- {meta_ads_mcp-0.2.1 → meta_ads_mcp-0.2.2}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meta-ads-mcp
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Model Calling Protocol (MCP) plugin for interacting with Meta Ads API
|
|
5
5
|
Project-URL: Homepage, https://github.com/nictuku/meta-ads-mcp
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/nictuku/meta-ads-mcp/issues
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Future Improvements for Meta Ads MCP
|
|
2
|
+
|
|
3
|
+
## Planned Improvement: Refactored meta_ads_generated.py
|
|
4
|
+
|
|
5
|
+
This file currently has too much code and it needs to be split into a small number of modules.
|
|
6
|
+
|
|
7
|
+
## Planned Improvement: Ad Set Update Confirmation Flow
|
|
8
|
+
|
|
9
|
+
### Overview
|
|
10
|
+
Currently, the `update_adset` function directly applies changes to Meta Ad Sets without user confirmation. This is potentially risky as users may not fully understand the impact of their changes before they're applied. We plan to implement a confirmation flow similar to the authentication flow that already exists for the `get_login_link` function.
|
|
11
|
+
|
|
12
|
+
### Requirements
|
|
13
|
+
|
|
14
|
+
1. **Local Confirmation Server:**
|
|
15
|
+
- Leverage the existing local HTTP server infrastructure used for authentication
|
|
16
|
+
- Create a new endpoint at `/confirm-update` to display proposed changes
|
|
17
|
+
- Ensure the server can handle multiple types of confirmation flows
|
|
18
|
+
|
|
19
|
+
2. **Update Flow:**
|
|
20
|
+
- When `update_adset` is called, it will:
|
|
21
|
+
- Fetch the current ad set details to establish a baseline
|
|
22
|
+
- Generate a diff between current and proposed changes
|
|
23
|
+
- Return a clickable link to a local confirmation page instead of making immediate changes
|
|
24
|
+
|
|
25
|
+
3. **Confirmation UI:**
|
|
26
|
+
- Display ad set name and ID clearly at the top
|
|
27
|
+
- Show a side-by-side or diff view of the current vs. proposed settings
|
|
28
|
+
- Highlight specific changes (e.g., bid amount, bid strategy, status changes)
|
|
29
|
+
- Provide "Approve" and "Cancel" buttons
|
|
30
|
+
- Include a warning about potential billing impact for status changes
|
|
31
|
+
|
|
32
|
+
4. **Security Considerations:**
|
|
33
|
+
- Ensure that confirmation links have a short expiration time
|
|
34
|
+
- Generate unique tokens for each confirmation request to prevent replays
|
|
35
|
+
- Log all confirmation activities
|
|
36
|
+
|
|
37
|
+
5. **Response Handling:**
|
|
38
|
+
- If approved, execute the original update API call
|
|
39
|
+
- Return a success message with details of the changes applied
|
|
40
|
+
- If rejected, return a cancellation message
|
|
41
|
+
- Either way, provide detailed information to the user
|
|
42
|
+
|
|
43
|
+
6. **Implementation Timeline:**
|
|
44
|
+
- Phase 1: Implement the basic confirmation flow structure
|
|
45
|
+
- Phase 2: Add improved diff visualization
|
|
46
|
+
- Phase 3: Extend to other modification functions (campaigns, ads, etc.)
|
|
47
|
+
|
|
48
|
+
### Implementation Notes
|
|
49
|
+
|
|
50
|
+
The implementation will reuse much of the callback server infrastructure that already exists for authentication, adapting it to support this new use case. The confirmation page will be a simple HTML/JS application that can:
|
|
51
|
+
|
|
52
|
+
1. Parse URL parameters containing update details
|
|
53
|
+
2. Fetch current ad set state using the provided access token
|
|
54
|
+
3. Generate and display a visual diff
|
|
55
|
+
4. Send the confirmation back to the server via API call
|
|
56
|
+
|
|
57
|
+
By ensuring changes are confirmed before execution, we'll significantly reduce the risk of accidental modifications to ad campaigns while maintaining the flexibility and power of the Meta Ads MCP.
|
|
58
|
+
|
|
59
|
+
Future improvements can be added to this file as needed.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Meta Ads MCP - Command Line Entry Point
|
|
6
|
+
|
|
7
|
+
This script provides the command-line entry point for the Meta Ads MCP package.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from meta_ads_mcp import main
|
|
11
|
+
|
|
12
|
+
if __name__ == "__main__":
|
|
13
|
+
main()
|
|
@@ -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.2"
|
|
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
|
|
@@ -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)
|
|
@@ -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)}"
|