meta-ads-mcp 0.2.4__py3-none-any.whl → 0.2.5__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/core/adsets.py +32 -2
- meta_ads_mcp/core/api.py +68 -24
- meta_ads_mcp/core/auth.py +1212 -382
- meta_ads_mcp/core/authentication.py +10 -1
- meta_ads_mcp/core/utils.py +10 -0
- {meta_ads_mcp-0.2.4.dist-info → meta_ads_mcp-0.2.5.dist-info}/METADATA +21 -11
- {meta_ads_mcp-0.2.4.dist-info → meta_ads_mcp-0.2.5.dist-info}/RECORD +10 -10
- {meta_ads_mcp-0.2.4.dist-info → meta_ads_mcp-0.2.5.dist-info}/WHEEL +0 -0
- {meta_ads_mcp-0.2.4.dist-info → meta_ads_mcp-0.2.5.dist-info}/entry_points.txt +0 -0
meta_ads_mcp/__init__.py
CHANGED
meta_ads_mcp/core/adsets.py
CHANGED
|
@@ -7,6 +7,7 @@ from .accounts import get_ad_accounts
|
|
|
7
7
|
from .server import mcp_server
|
|
8
8
|
import asyncio
|
|
9
9
|
from .auth import start_callback_server, update_confirmation
|
|
10
|
+
import urllib.parse
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
@mcp_server.tool()
|
|
@@ -77,7 +78,7 @@ async def get_adset_details(access_token: str = None, adset_id: str = None) -> s
|
|
|
77
78
|
@mcp_server.tool()
|
|
78
79
|
@meta_api_tool
|
|
79
80
|
async def update_adset(adset_id: str, frequency_control_specs: List[Dict[str, Any]] = None, bid_strategy: str = None,
|
|
80
|
-
bid_amount: int = None, status: str = None, access_token: str = None) -> str:
|
|
81
|
+
bid_amount: int = None, status: str = None, targeting: Dict[str, Any] = None, access_token: str = None) -> str:
|
|
81
82
|
"""
|
|
82
83
|
Update an ad set with new settings including frequency caps.
|
|
83
84
|
|
|
@@ -88,6 +89,8 @@ async def update_adset(adset_id: str, frequency_control_specs: List[Dict[str, An
|
|
|
88
89
|
bid_strategy: Bid strategy (e.g., 'LOWEST_COST_WITH_BID_CAP')
|
|
89
90
|
bid_amount: Bid amount in account currency (in cents for USD)
|
|
90
91
|
status: Update ad set status (ACTIVE, PAUSED, etc.)
|
|
92
|
+
targeting: Targeting specifications including targeting_automation
|
|
93
|
+
(e.g. {"targeting_automation":{"advantage_audience":1}})
|
|
91
94
|
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
92
95
|
"""
|
|
93
96
|
if not adset_id:
|
|
@@ -106,6 +109,32 @@ async def update_adset(adset_id: str, frequency_control_specs: List[Dict[str, An
|
|
|
106
109
|
|
|
107
110
|
if status is not None:
|
|
108
111
|
changes['status'] = status
|
|
112
|
+
|
|
113
|
+
if targeting is not None:
|
|
114
|
+
# Get current ad set details to preserve existing targeting settings
|
|
115
|
+
current_details_json = await get_adset_details(adset_id=adset_id, access_token=access_token)
|
|
116
|
+
current_details = json.loads(current_details_json)
|
|
117
|
+
|
|
118
|
+
# Check if the current ad set has targeting information
|
|
119
|
+
current_targeting = current_details.get('targeting', {})
|
|
120
|
+
|
|
121
|
+
if 'targeting_automation' in targeting:
|
|
122
|
+
# Only update targeting_automation while preserving other targeting settings
|
|
123
|
+
if current_targeting:
|
|
124
|
+
merged_targeting = current_targeting.copy()
|
|
125
|
+
merged_targeting['targeting_automation'] = targeting['targeting_automation']
|
|
126
|
+
changes['targeting'] = merged_targeting
|
|
127
|
+
else:
|
|
128
|
+
# If there's no existing targeting, we need to create a basic one
|
|
129
|
+
# Meta requires at least a geo_locations setting
|
|
130
|
+
basic_targeting = {
|
|
131
|
+
'targeting_automation': targeting['targeting_automation'],
|
|
132
|
+
'geo_locations': {'countries': ['US']} # Using US as default location
|
|
133
|
+
}
|
|
134
|
+
changes['targeting'] = basic_targeting
|
|
135
|
+
else:
|
|
136
|
+
# Full targeting replacement
|
|
137
|
+
changes['targeting'] = targeting
|
|
109
138
|
|
|
110
139
|
if not changes:
|
|
111
140
|
return json.dumps({"error": "No update parameters provided"}, indent=2)
|
|
@@ -119,7 +148,8 @@ async def update_adset(adset_id: str, frequency_control_specs: List[Dict[str, An
|
|
|
119
148
|
|
|
120
149
|
# Generate confirmation URL with properly encoded parameters
|
|
121
150
|
changes_json = json.dumps(changes)
|
|
122
|
-
|
|
151
|
+
encoded_changes = urllib.parse.quote(changes_json)
|
|
152
|
+
confirmation_url = f"http://localhost:{port}/confirm-update?adset_id={adset_id}&token={access_token}&changes={encoded_changes}"
|
|
123
153
|
|
|
124
154
|
# Reset the update confirmation
|
|
125
155
|
update_confirmation.clear()
|
meta_ads_mcp/core/api.py
CHANGED
|
@@ -59,9 +59,9 @@ async def make_api_request(
|
|
|
59
59
|
if not access_token:
|
|
60
60
|
logger.error("API request attempted with blank access token")
|
|
61
61
|
return {
|
|
62
|
-
"error":
|
|
63
|
-
|
|
64
|
-
"
|
|
62
|
+
"error": {
|
|
63
|
+
"message": "Authentication Required",
|
|
64
|
+
"details": "A valid access token is required to access the Meta API",
|
|
65
65
|
"action_required": "Please authenticate first"
|
|
66
66
|
}
|
|
67
67
|
}
|
|
@@ -89,7 +89,18 @@ async def make_api_request(
|
|
|
89
89
|
if method == "GET":
|
|
90
90
|
response = await client.get(url, params=request_params, headers=headers, timeout=30.0)
|
|
91
91
|
elif method == "POST":
|
|
92
|
-
|
|
92
|
+
# For Meta API, POST requests need data, not JSON
|
|
93
|
+
if 'targeting' in request_params and isinstance(request_params['targeting'], dict):
|
|
94
|
+
# Convert targeting dict to string for the API
|
|
95
|
+
request_params['targeting'] = json.dumps(request_params['targeting'])
|
|
96
|
+
|
|
97
|
+
# Convert lists and dicts to JSON strings
|
|
98
|
+
for key, value in request_params.items():
|
|
99
|
+
if isinstance(value, (list, dict)):
|
|
100
|
+
request_params[key] = json.dumps(value)
|
|
101
|
+
|
|
102
|
+
logger.debug(f"POST params (prepared): {masked_params}")
|
|
103
|
+
response = await client.post(url, data=request_params, headers=headers, timeout=30.0)
|
|
93
104
|
elif method == "DELETE":
|
|
94
105
|
response = await client.delete(url, params=request_params, headers=headers, timeout=30.0)
|
|
95
106
|
else:
|
|
@@ -97,7 +108,16 @@ async def make_api_request(
|
|
|
97
108
|
|
|
98
109
|
response.raise_for_status()
|
|
99
110
|
logger.debug(f"API Response status: {response.status_code}")
|
|
100
|
-
|
|
111
|
+
|
|
112
|
+
# Ensure the response is JSON and return it as a dictionary
|
|
113
|
+
try:
|
|
114
|
+
return response.json()
|
|
115
|
+
except json.JSONDecodeError:
|
|
116
|
+
# If not JSON, return text content in a structured format
|
|
117
|
+
return {
|
|
118
|
+
"text_response": response.text,
|
|
119
|
+
"status_code": response.status_code
|
|
120
|
+
}
|
|
101
121
|
|
|
102
122
|
except httpx.HTTPStatusError as e:
|
|
103
123
|
error_info = {}
|
|
@@ -123,8 +143,7 @@ async def make_api_request(
|
|
|
123
143
|
logger.error(f"Current app_id: {app_id}")
|
|
124
144
|
# Provide a clearer error message without the confusing "Provide valid app ID" message
|
|
125
145
|
return {
|
|
126
|
-
"error":
|
|
127
|
-
"details": {
|
|
146
|
+
"error": {
|
|
128
147
|
"message": "Meta API authentication configuration issue. Please check your app credentials.",
|
|
129
148
|
"original_error": error_obj.get("message"),
|
|
130
149
|
"code": error_obj.get("code")
|
|
@@ -132,11 +151,28 @@ async def make_api_request(
|
|
|
132
151
|
}
|
|
133
152
|
auth_manager.invalidate_token()
|
|
134
153
|
|
|
135
|
-
|
|
154
|
+
# Include full details for technical users
|
|
155
|
+
full_response = {
|
|
156
|
+
"headers": dict(e.response.headers),
|
|
157
|
+
"status_code": e.response.status_code,
|
|
158
|
+
"url": str(e.response.url),
|
|
159
|
+
"reason": getattr(e.response, "reason_phrase", "Unknown reason"),
|
|
160
|
+
"request_method": e.request.method,
|
|
161
|
+
"request_url": str(e.request.url)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
# Return a properly structured error object
|
|
165
|
+
return {
|
|
166
|
+
"error": {
|
|
167
|
+
"message": f"HTTP Error: {e.response.status_code}",
|
|
168
|
+
"details": error_info,
|
|
169
|
+
"full_response": full_response
|
|
170
|
+
}
|
|
171
|
+
}
|
|
136
172
|
|
|
137
173
|
except Exception as e:
|
|
138
174
|
logger.error(f"Request Error: {str(e)}")
|
|
139
|
-
return {"error": str(e)}
|
|
175
|
+
return {"error": {"message": str(e)}}
|
|
140
176
|
|
|
141
177
|
|
|
142
178
|
# Generic wrapper for all Meta API tools
|
|
@@ -174,12 +210,14 @@ def meta_api_tool(func):
|
|
|
174
210
|
logger.warning("No access token available, authentication needed")
|
|
175
211
|
auth_url = auth_manager.get_auth_url()
|
|
176
212
|
return json.dumps({
|
|
177
|
-
"error":
|
|
178
|
-
|
|
179
|
-
"
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
213
|
+
"error": {
|
|
214
|
+
"message": "Authentication Required",
|
|
215
|
+
"details": {
|
|
216
|
+
"description": "You need to authenticate with the Meta API before using this tool",
|
|
217
|
+
"action_required": "Please authenticate first",
|
|
218
|
+
"auth_url": auth_url,
|
|
219
|
+
"markdown_link": f"[Click here to authenticate with Meta Ads API]({auth_url})"
|
|
220
|
+
}
|
|
183
221
|
}
|
|
184
222
|
}, indent=2)
|
|
185
223
|
|
|
@@ -200,18 +238,24 @@ def meta_api_tool(func):
|
|
|
200
238
|
logger.error(f"Current app_id: {app_id}")
|
|
201
239
|
# Replace the confusing error with a more user-friendly one
|
|
202
240
|
return json.dumps({
|
|
203
|
-
"error":
|
|
204
|
-
|
|
205
|
-
"
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
241
|
+
"error": {
|
|
242
|
+
"message": "Meta API Configuration Issue",
|
|
243
|
+
"details": {
|
|
244
|
+
"description": "Your Meta API app is not properly configured",
|
|
245
|
+
"action_required": "Check your META_APP_ID environment variable",
|
|
246
|
+
"current_app_id": app_id,
|
|
247
|
+
"original_error": error_obj.get("message")
|
|
248
|
+
}
|
|
209
249
|
}
|
|
210
250
|
}, indent=2)
|
|
211
251
|
except Exception:
|
|
212
|
-
# Not JSON or other parsing error,
|
|
213
|
-
|
|
214
|
-
|
|
252
|
+
# Not JSON or other parsing error, wrap it in a dictionary
|
|
253
|
+
return json.dumps({"data": result}, indent=2)
|
|
254
|
+
|
|
255
|
+
# If result is already a dictionary, ensure it's properly serialized
|
|
256
|
+
if isinstance(result, dict):
|
|
257
|
+
return json.dumps(result, indent=2)
|
|
258
|
+
|
|
215
259
|
return result
|
|
216
260
|
except Exception as e:
|
|
217
261
|
logger.error(f"Error in {func.__name__}: {str(e)}")
|