meta-ads-mcp 0.8.0__tar.gz → 0.9.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.8.0 → meta_ads_mcp-0.9.1}/PKG-INFO +2 -2
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/__init__.py +1 -1
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/ads.py +160 -127
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/api.py +36 -15
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/insights.py +23 -1
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/openai_deep_research.py +46 -1
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/pipeboard_auth.py +3 -15
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/server.py +1 -1
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/pyproject.toml +2 -2
- meta_ads_mcp-0.9.1/tests/test_get_account_pages.py +399 -0
- meta_ads_mcp-0.9.1/tests/test_openai.py +29 -0
- meta_ads_mcp-0.9.1/tests/test_update_ad_creative_id.py +339 -0
- meta_ads_mcp-0.8.0/tests/test_openai.py +0 -23
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/.github/workflows/publish.yml +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/.github/workflows/test.yml +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/.gitignore +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/CUSTOM_META_APP.md +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/Dockerfile +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/LICENSE +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/LOCAL_INSTALLATION.md +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/META_API_NOTES.md +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/README.md +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/RELEASE.md +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/STREAMABLE_HTTP_SETUP.md +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/examples/README.md +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/examples/example_http_client.py +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/future_improvements.md +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/images/meta-ads-example.png +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_auth.sh +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/__main__.py +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/__init__.py +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/accounts.py +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/ads_library.py +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/adsets.py +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/auth.py +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/authentication.py +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/budget_schedules.py +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/callback_server.py +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/campaigns.py +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/duplication.py +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/http_auth_integration.py +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/reports.py +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/resources.py +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/targeting.py +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/utils.py +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/requirements.txt +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/setup.py +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/smithery.yaml +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/README.md +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/README_REGRESSION_TESTS.md +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/__init__.py +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/conftest.py +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/test_account_search.py +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/test_budget_update.py +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/test_budget_update_e2e.py +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/test_dsa_beneficiary.py +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/test_dsa_integration.py +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/test_duplication.py +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/test_duplication_regression.py +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/test_dynamic_creatives.py +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/test_get_ad_creatives_fix.py +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/test_get_ad_image_quality_improvements.py +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/test_get_ad_image_regression.py +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/test_http_transport.py +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/test_insights_actions_and_values.py +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/test_integration_openai_mcp.py +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/test_openai_mcp_deep_research.py +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/test_page_discovery.py +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/test_page_discovery_integration.py +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/test_targeting.py +0 -0
- {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/test_targeting_search_e2e.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.9.1
|
|
4
4
|
Summary: Model Context Protocol (MCP) plugin 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
|
|
@@ -13,7 +13,7 @@ Classifier: Operating System :: OS Independent
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3
|
|
14
14
|
Requires-Python: >=3.10
|
|
15
15
|
Requires-Dist: httpx>=0.26.0
|
|
16
|
-
Requires-Dist: mcp[cli]
|
|
16
|
+
Requires-Dist: mcp[cli]<=1.12.2,>=1.10.1
|
|
17
17
|
Requires-Dist: pathlib>=1.0.1
|
|
18
18
|
Requires-Dist: pillow>=10.0.0
|
|
19
19
|
Requires-Dist: pytest-asyncio>=1.0.0
|
|
@@ -519,6 +519,7 @@ async def update_ad(
|
|
|
519
519
|
status: str = None,
|
|
520
520
|
bid_amount: int = None,
|
|
521
521
|
tracking_specs = None,
|
|
522
|
+
creative_id: str = None,
|
|
522
523
|
access_token: str = None
|
|
523
524
|
) -> str:
|
|
524
525
|
"""
|
|
@@ -529,6 +530,7 @@ async def update_ad(
|
|
|
529
530
|
status: Update ad status (ACTIVE, PAUSED, etc.)
|
|
530
531
|
bid_amount: Bid amount in account currency (in cents for USD)
|
|
531
532
|
tracking_specs: Optional tracking specifications (e.g., for pixel events).
|
|
533
|
+
creative_id: ID of the creative to associate with this ad (changes the ad's image/content)
|
|
532
534
|
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
533
535
|
"""
|
|
534
536
|
if not ad_id:
|
|
@@ -542,14 +544,19 @@ async def update_ad(
|
|
|
542
544
|
params["bid_amount"] = str(bid_amount)
|
|
543
545
|
if tracking_specs is not None: # Add tracking_specs to params if provided
|
|
544
546
|
params["tracking_specs"] = json.dumps(tracking_specs) # Needs to be JSON encoded string
|
|
547
|
+
if creative_id is not None:
|
|
548
|
+
# Creative parameter needs to be a JSON object containing creative_id
|
|
549
|
+
params["creative"] = json.dumps({"creative_id": creative_id})
|
|
545
550
|
|
|
546
551
|
if not params:
|
|
547
|
-
return json.dumps({"error": "No update parameters provided (status, bid_amount, or
|
|
552
|
+
return json.dumps({"error": "No update parameters provided (status, bid_amount, tracking_specs, or creative_id)"}, indent=2)
|
|
548
553
|
|
|
549
554
|
endpoint = f"{ad_id}"
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
555
|
+
try:
|
|
556
|
+
data = await make_api_request(endpoint, access_token, params, method='POST')
|
|
557
|
+
return json.dumps(data, indent=2)
|
|
558
|
+
except Exception as e:
|
|
559
|
+
return json.dumps({"error": f"Failed to update ad: {str(e)}"}, indent=2)
|
|
553
560
|
|
|
554
561
|
|
|
555
562
|
@mcp_server.tool()
|
|
@@ -1205,69 +1212,143 @@ async def get_account_pages(access_token: str = None, account_id: str = None) ->
|
|
|
1205
1212
|
account_id = f"act_{account_id}"
|
|
1206
1213
|
|
|
1207
1214
|
try:
|
|
1208
|
-
#
|
|
1209
|
-
|
|
1210
|
-
# Approach 1: Get active ads and extract page IDs
|
|
1211
|
-
endpoint = f"{account_id}/ads"
|
|
1212
|
-
params = {
|
|
1213
|
-
"fields": "creative{object_story_spec{page_id}}",
|
|
1214
|
-
"limit": 100
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
ads_data = await make_api_request(endpoint, access_token, params)
|
|
1218
|
-
|
|
1219
|
-
# Extract unique page IDs from ads
|
|
1220
|
-
page_ids = set()
|
|
1221
|
-
if "data" in ads_data:
|
|
1222
|
-
for ad in ads_data.get("data", []):
|
|
1223
|
-
if "creative" in ad and "creative" in ad and "object_story_spec" in ad["creative"] and "page_id" in ad["creative"]["object_story_spec"]:
|
|
1224
|
-
page_ids.add(ad["creative"]["object_story_spec"]["page_id"])
|
|
1215
|
+
# Collect all page IDs from multiple approaches
|
|
1216
|
+
all_page_ids = set()
|
|
1225
1217
|
|
|
1226
|
-
#
|
|
1227
|
-
|
|
1228
|
-
|
|
1218
|
+
# Approach 1: Get user's personal pages (broad scope)
|
|
1219
|
+
try:
|
|
1220
|
+
endpoint = "me/accounts"
|
|
1221
|
+
params = {
|
|
1222
|
+
"fields": "id,name,username,category,fan_count,link,verification_status,picture"
|
|
1223
|
+
}
|
|
1224
|
+
user_pages_data = await make_api_request(endpoint, access_token, params)
|
|
1225
|
+
if "data" in user_pages_data:
|
|
1226
|
+
for page in user_pages_data["data"]:
|
|
1227
|
+
if "id" in page:
|
|
1228
|
+
all_page_ids.add(page["id"])
|
|
1229
|
+
except Exception:
|
|
1230
|
+
pass
|
|
1231
|
+
|
|
1232
|
+
# Approach 2: Try business manager pages
|
|
1233
|
+
try:
|
|
1234
|
+
# Strip 'act_' prefix to get raw account ID for business endpoints
|
|
1235
|
+
raw_account_id = account_id.replace("act_", "")
|
|
1236
|
+
endpoint = f"{raw_account_id}/owned_pages"
|
|
1237
|
+
params = {
|
|
1238
|
+
"fields": "id,name,username,category,fan_count,link,verification_status,picture"
|
|
1239
|
+
}
|
|
1240
|
+
business_pages_data = await make_api_request(endpoint, access_token, params)
|
|
1241
|
+
if "data" in business_pages_data:
|
|
1242
|
+
for page in business_pages_data["data"]:
|
|
1243
|
+
if "id" in page:
|
|
1244
|
+
all_page_ids.add(page["id"])
|
|
1245
|
+
except Exception:
|
|
1246
|
+
pass
|
|
1247
|
+
|
|
1248
|
+
# Approach 3: Try ad account client pages
|
|
1249
|
+
try:
|
|
1250
|
+
endpoint = f"{account_id}/client_pages"
|
|
1251
|
+
params = {
|
|
1252
|
+
"fields": "id,name,username,category,fan_count,link,verification_status,picture"
|
|
1253
|
+
}
|
|
1254
|
+
client_pages_data = await make_api_request(endpoint, access_token, params)
|
|
1255
|
+
if "data" in client_pages_data:
|
|
1256
|
+
for page in client_pages_data["data"]:
|
|
1257
|
+
if "id" in page:
|
|
1258
|
+
all_page_ids.add(page["id"])
|
|
1259
|
+
except Exception:
|
|
1260
|
+
pass
|
|
1261
|
+
|
|
1262
|
+
# Approach 4: Extract page IDs from all ad creatives (broader creative search)
|
|
1263
|
+
try:
|
|
1264
|
+
endpoint = f"{account_id}/adcreatives"
|
|
1265
|
+
params = {
|
|
1266
|
+
"fields": "id,name,object_story_spec,link_url,call_to_action,image_hash",
|
|
1267
|
+
"limit": 100
|
|
1268
|
+
}
|
|
1269
|
+
creatives_data = await make_api_request(endpoint, access_token, params)
|
|
1270
|
+
if "data" in creatives_data:
|
|
1271
|
+
for creative in creatives_data["data"]:
|
|
1272
|
+
if "object_story_spec" in creative and "page_id" in creative["object_story_spec"]:
|
|
1273
|
+
all_page_ids.add(creative["object_story_spec"]["page_id"])
|
|
1274
|
+
except Exception:
|
|
1275
|
+
pass
|
|
1229
1276
|
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1277
|
+
# Approach 5: Get active ads and extract page IDs from creatives
|
|
1278
|
+
try:
|
|
1279
|
+
endpoint = f"{account_id}/ads"
|
|
1280
|
+
params = {
|
|
1281
|
+
"fields": "creative{object_story_spec{page_id},link_url,call_to_action}",
|
|
1282
|
+
"limit": 100
|
|
1283
|
+
}
|
|
1284
|
+
ads_data = await make_api_request(endpoint, access_token, params)
|
|
1285
|
+
if "data" in ads_data:
|
|
1286
|
+
for ad in ads_data.get("data", []):
|
|
1287
|
+
if "creative" in ad and "object_story_spec" in ad["creative"] and "page_id" in ad["creative"]["object_story_spec"]:
|
|
1288
|
+
all_page_ids.add(ad["creative"]["object_story_spec"]["page_id"])
|
|
1289
|
+
except Exception:
|
|
1290
|
+
pass
|
|
1291
|
+
|
|
1292
|
+
# Approach 6: Try promoted_objects endpoint
|
|
1293
|
+
try:
|
|
1294
|
+
endpoint = f"{account_id}/promoted_objects"
|
|
1295
|
+
params = {
|
|
1296
|
+
"fields": "page_id,object_store_url,product_set_id,application_id"
|
|
1297
|
+
}
|
|
1298
|
+
promoted_objects_data = await make_api_request(endpoint, access_token, params)
|
|
1299
|
+
if "data" in promoted_objects_data:
|
|
1300
|
+
for obj in promoted_objects_data["data"]:
|
|
1301
|
+
if "page_id" in obj:
|
|
1302
|
+
all_page_ids.add(obj["page_id"])
|
|
1303
|
+
except Exception:
|
|
1304
|
+
pass
|
|
1305
|
+
|
|
1306
|
+
# Approach 7: Extract page IDs from tracking_specs in ads (most reliable)
|
|
1307
|
+
try:
|
|
1308
|
+
endpoint = f"{account_id}/ads"
|
|
1309
|
+
params = {
|
|
1310
|
+
"fields": "id,name,status,creative,tracking_specs",
|
|
1311
|
+
"limit": 100
|
|
1312
|
+
}
|
|
1313
|
+
tracking_ads_data = await make_api_request(endpoint, access_token, params)
|
|
1314
|
+
if "data" in tracking_ads_data:
|
|
1315
|
+
for ad in tracking_ads_data.get("data", []):
|
|
1316
|
+
tracking_specs = ad.get("tracking_specs", [])
|
|
1317
|
+
if isinstance(tracking_specs, list):
|
|
1318
|
+
for spec in tracking_specs:
|
|
1319
|
+
if isinstance(spec, dict) and "page" in spec:
|
|
1320
|
+
page_list = spec["page"]
|
|
1321
|
+
if isinstance(page_list, list):
|
|
1322
|
+
for page_id in page_list:
|
|
1323
|
+
if isinstance(page_id, (str, int)) and str(page_id).isdigit():
|
|
1324
|
+
all_page_ids.add(str(page_id))
|
|
1325
|
+
except Exception:
|
|
1326
|
+
pass
|
|
1239
1327
|
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
if "data" in promoted_objects_data and promoted_objects_data["data"]:
|
|
1263
|
-
page_ids = set()
|
|
1264
|
-
for obj in promoted_objects_data["data"]:
|
|
1265
|
-
if "page_id" in obj:
|
|
1266
|
-
page_ids.add(obj["page_id"])
|
|
1328
|
+
# Approach 8: Try campaigns and extract page info
|
|
1329
|
+
try:
|
|
1330
|
+
endpoint = f"{account_id}/campaigns"
|
|
1331
|
+
params = {
|
|
1332
|
+
"fields": "id,name,promoted_object,objective",
|
|
1333
|
+
"limit": 50
|
|
1334
|
+
}
|
|
1335
|
+
campaigns_data = await make_api_request(endpoint, access_token, params)
|
|
1336
|
+
if "data" in campaigns_data:
|
|
1337
|
+
for campaign in campaigns_data["data"]:
|
|
1338
|
+
if "promoted_object" in campaign and "page_id" in campaign["promoted_object"]:
|
|
1339
|
+
all_page_ids.add(campaign["promoted_object"]["page_id"])
|
|
1340
|
+
except Exception:
|
|
1341
|
+
pass
|
|
1342
|
+
|
|
1343
|
+
# If we found any page IDs, get details for each
|
|
1344
|
+
if all_page_ids:
|
|
1345
|
+
page_details = {
|
|
1346
|
+
"data": [],
|
|
1347
|
+
"total_pages_found": len(all_page_ids)
|
|
1348
|
+
}
|
|
1267
1349
|
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
for page_id in page_ids:
|
|
1350
|
+
for page_id in all_page_ids:
|
|
1351
|
+
try:
|
|
1271
1352
|
page_endpoint = f"{page_id}"
|
|
1272
1353
|
page_params = {
|
|
1273
1354
|
"fields": "id,name,username,category,fan_count,link,verification_status,picture"
|
|
@@ -1276,62 +1357,15 @@ async def get_account_pages(access_token: str = None, account_id: str = None) ->
|
|
|
1276
1357
|
page_data = await make_api_request(page_endpoint, access_token, page_params)
|
|
1277
1358
|
if "id" in page_data:
|
|
1278
1359
|
page_details["data"].append(page_data)
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
# This approach is often the most reliable as confirmed by community feedback
|
|
1286
|
-
endpoint = f"{account_id}/ads"
|
|
1287
|
-
params = {
|
|
1288
|
-
"fields": "id,name,adset_id,campaign_id,status,creative,created_time,updated_time,bid_amount,conversion_domain,tracking_specs",
|
|
1289
|
-
"limit": 100
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
tracking_ads_data = await make_api_request(endpoint, access_token, params)
|
|
1293
|
-
|
|
1294
|
-
tracking_page_ids = set()
|
|
1295
|
-
if "data" in tracking_ads_data:
|
|
1296
|
-
for ad in tracking_ads_data.get("data", []):
|
|
1297
|
-
tracking_specs = ad.get("tracking_specs", [])
|
|
1298
|
-
if isinstance(tracking_specs, list):
|
|
1299
|
-
for spec in tracking_specs:
|
|
1300
|
-
# If 'page' key exists, add all page IDs
|
|
1301
|
-
if isinstance(spec, dict) and "page" in spec:
|
|
1302
|
-
page_list = spec["page"]
|
|
1303
|
-
if isinstance(page_list, list):
|
|
1304
|
-
for page_id in page_list:
|
|
1305
|
-
# Validate page ID format (should be numeric string)
|
|
1306
|
-
if isinstance(page_id, (str, int)) and str(page_id).isdigit():
|
|
1307
|
-
tracking_page_ids.add(str(page_id))
|
|
1308
|
-
|
|
1309
|
-
if tracking_page_ids:
|
|
1310
|
-
page_details = {"data": [], "source": "tracking_specs", "note": "Page IDs extracted from active ads - these are the most reliable for ad creation"}
|
|
1311
|
-
for page_id in tracking_page_ids:
|
|
1312
|
-
page_endpoint = f"{page_id}"
|
|
1313
|
-
page_params = {
|
|
1314
|
-
"fields": "id,name,username,category,fan_count,link,verification_status,picture"
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
page_data = await make_api_request(page_endpoint, access_token, page_params)
|
|
1318
|
-
if "id" in page_data:
|
|
1319
|
-
# Add additional context about this page ID being suitable for ads
|
|
1320
|
-
page_data["_meta"] = {
|
|
1321
|
-
"suitable_for_ads": True,
|
|
1322
|
-
"found_in_tracking_specs": True,
|
|
1323
|
-
"recommended_for_create_ad_creative": True
|
|
1324
|
-
}
|
|
1325
|
-
page_details["data"].append(page_data)
|
|
1326
|
-
else:
|
|
1360
|
+
else:
|
|
1361
|
+
page_details["data"].append({
|
|
1362
|
+
"id": page_id,
|
|
1363
|
+
"error": "Page details not accessible"
|
|
1364
|
+
})
|
|
1365
|
+
except Exception as e:
|
|
1327
1366
|
page_details["data"].append({
|
|
1328
|
-
"id": page_id,
|
|
1329
|
-
"error": "
|
|
1330
|
-
"_meta": {
|
|
1331
|
-
"suitable_for_ads": True,
|
|
1332
|
-
"found_in_tracking_specs": True,
|
|
1333
|
-
"note": "Page ID exists in ads but details not accessible - you can still use this ID for ad creation"
|
|
1334
|
-
}
|
|
1367
|
+
"id": page_id,
|
|
1368
|
+
"error": f"Failed to get page details: {str(e)}"
|
|
1335
1369
|
})
|
|
1336
1370
|
|
|
1337
1371
|
if page_details["data"]:
|
|
@@ -1340,18 +1374,17 @@ async def get_account_pages(access_token: str = None, account_id: str = None) ->
|
|
|
1340
1374
|
# If all approaches failed, return empty data with a message
|
|
1341
1375
|
return json.dumps({
|
|
1342
1376
|
"data": [],
|
|
1343
|
-
"message": "No pages found associated with this account
|
|
1344
|
-
"
|
|
1345
|
-
"suggestion_1": "If you have existing ads, run 'get_ads' and look for page IDs in the 'tracking_specs' field",
|
|
1346
|
-
"suggestion_2": "Use the exact page ID from existing ads' tracking_specs for creating new ad creatives",
|
|
1347
|
-
"suggestion_3": "Verify your page ID format - it should be a numeric string (e.g., '123456789')",
|
|
1348
|
-
"suggestion_4": "Check for digit transpositions or formatting errors in your page ID"
|
|
1349
|
-
},
|
|
1350
|
-
"note": "Based on community feedback, page IDs from existing ads' tracking_specs are the most reliable for ad creation"
|
|
1377
|
+
"message": "No pages found associated with this account",
|
|
1378
|
+
"suggestion": "Create a Facebook page and connect it to this ad account, or ensure existing pages are properly connected through Business Manager"
|
|
1351
1379
|
}, indent=2)
|
|
1352
1380
|
|
|
1353
1381
|
except Exception as e:
|
|
1354
1382
|
return json.dumps({
|
|
1355
1383
|
"error": "Failed to get account pages",
|
|
1356
1384
|
"details": str(e)
|
|
1357
|
-
}, indent=2)
|
|
1385
|
+
}, indent=2)
|
|
1386
|
+
|
|
1387
|
+
|
|
1388
|
+
|
|
1389
|
+
|
|
1390
|
+
|
|
@@ -240,22 +240,43 @@ def meta_api_tool(func):
|
|
|
240
240
|
logger.error("ISSUE DETECTED: Pipeboard authentication configured but no valid token available")
|
|
241
241
|
logger.error("ACTION REQUIRED: Complete authentication via Pipeboard service")
|
|
242
242
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
"
|
|
247
|
-
"
|
|
248
|
-
"
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
"
|
|
252
|
-
"
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
243
|
+
# Provide different guidance based on authentication method
|
|
244
|
+
if using_pipeboard:
|
|
245
|
+
return json.dumps({
|
|
246
|
+
"error": {
|
|
247
|
+
"message": "Pipeboard Authentication Required",
|
|
248
|
+
"details": {
|
|
249
|
+
"description": "Your Pipeboard API token is invalid or has expired",
|
|
250
|
+
"action_required": "Update your Pipeboard token",
|
|
251
|
+
"setup_url": "https://pipeboard.co/setup",
|
|
252
|
+
"token_url": "https://pipeboard.co/api-tokens",
|
|
253
|
+
"configuration_status": {
|
|
254
|
+
"app_id_configured": bool(app_id) and app_id != "YOUR_META_APP_ID",
|
|
255
|
+
"pipeboard_enabled": True,
|
|
256
|
+
},
|
|
257
|
+
"troubleshooting": "Go to https://pipeboard.co/setup to verify your account setup, then visit https://pipeboard.co/api-tokens to obtain a new API token",
|
|
258
|
+
"setup_link": "[Verify your Pipeboard account setup](https://pipeboard.co/setup)",
|
|
259
|
+
"token_link": "[Get a new Pipeboard API token](https://pipeboard.co/api-tokens)"
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}, indent=2)
|
|
263
|
+
else:
|
|
264
|
+
return json.dumps({
|
|
265
|
+
"error": {
|
|
266
|
+
"message": "Authentication Required",
|
|
267
|
+
"details": {
|
|
268
|
+
"description": "You need to authenticate with the Meta API before using this tool",
|
|
269
|
+
"action_required": "Please authenticate first",
|
|
270
|
+
"auth_url": auth_url,
|
|
271
|
+
"configuration_status": {
|
|
272
|
+
"app_id_configured": bool(app_id) and app_id != "YOUR_META_APP_ID",
|
|
273
|
+
"pipeboard_enabled": False,
|
|
274
|
+
},
|
|
275
|
+
"troubleshooting": "Check logs for TOKEN VALIDATION FAILED messages",
|
|
276
|
+
"markdown_link": f"[Click here to authenticate with Meta Ads API]({auth_url})"
|
|
277
|
+
}
|
|
256
278
|
}
|
|
257
|
-
}
|
|
258
|
-
}, indent=2)
|
|
279
|
+
}, indent=2)
|
|
259
280
|
|
|
260
281
|
# Call the original function
|
|
261
282
|
result = await func(*args, **kwargs)
|
|
@@ -25,7 +25,29 @@ async def get_insights(access_token: str = None, object_id: str = None,
|
|
|
25
25
|
last_3d, last_7d, last_14d, last_28d, last_30d, last_90d, last_week_mon_sun,
|
|
26
26
|
last_week_sun_sat, last_quarter, last_year, this_week_mon_today, this_week_sun_today, this_year
|
|
27
27
|
Dictionary example: {"since":"2023-01-01","until":"2023-01-31"}
|
|
28
|
-
breakdown: Optional breakdown dimension
|
|
28
|
+
breakdown: Optional breakdown dimension. Valid values include:
|
|
29
|
+
Demographic: age, gender, country, region, dma
|
|
30
|
+
Platform/Device: device_platform, platform_position, publisher_platform, impression_device
|
|
31
|
+
Creative Assets: ad_format_asset, body_asset, call_to_action_asset, description_asset,
|
|
32
|
+
image_asset, link_url_asset, title_asset, video_asset, media_asset_url,
|
|
33
|
+
media_creator, media_destination_url, media_format, media_origin_url,
|
|
34
|
+
media_text_content, media_type, creative_relaxation_asset_type,
|
|
35
|
+
flexible_format_asset_type, gen_ai_asset_type
|
|
36
|
+
Campaign/Ad Attributes: breakdown_ad_objective, breakdown_reporting_ad_id, app_id, product_id
|
|
37
|
+
Conversion Tracking: coarse_conversion_value, conversion_destination, standard_event_content_type,
|
|
38
|
+
signal_source_bucket, is_conversion_id_modeled, fidelity_type, redownload
|
|
39
|
+
Time-based: hourly_stats_aggregated_by_advertiser_time_zone,
|
|
40
|
+
hourly_stats_aggregated_by_audience_time_zone, frequency_value
|
|
41
|
+
Extensions/Landing: ad_extension_domain, ad_extension_url, landing_destination,
|
|
42
|
+
mdsa_landing_destination
|
|
43
|
+
Attribution: sot_attribution_model_type, sot_attribution_window, sot_channel,
|
|
44
|
+
sot_event_type, sot_source
|
|
45
|
+
Mobile/SKAN: skan_campaign_id, skan_conversion_id, skan_version, postback_sequence_index
|
|
46
|
+
CRM/Business: crm_advertiser_l12_territory_ids, crm_advertiser_subvertical_id,
|
|
47
|
+
crm_advertiser_vertical_id, crm_ult_advertiser_id, user_persona_id, user_persona_name
|
|
48
|
+
Advanced: hsid, is_auto_advance, is_rendered_as_delayed_skip_ad, mmm, place_page_id,
|
|
49
|
+
marketing_messages_btn_name, impression_view_time_advertiser_hour_v2, comscore_market,
|
|
50
|
+
comscore_market_code
|
|
29
51
|
level: Level of aggregation (ad, adset, campaign, account)
|
|
30
52
|
"""
|
|
31
53
|
if not object_id:
|
|
@@ -103,6 +103,24 @@ class MetaAdsDataManager:
|
|
|
103
103
|
logger.error(f"Error fetching pages for {account_id}: {e}")
|
|
104
104
|
return []
|
|
105
105
|
|
|
106
|
+
async def _get_businesses(self, access_token: str, user_id: str = "me", limit: int = 25) -> List[Dict[str, Any]]:
|
|
107
|
+
"""Get businesses accessible by the current user"""
|
|
108
|
+
try:
|
|
109
|
+
endpoint = f"{user_id}/businesses"
|
|
110
|
+
params = {
|
|
111
|
+
"fields": "id,name,created_time,verification_status",
|
|
112
|
+
"limit": limit
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
data = await make_api_request(endpoint, access_token, params)
|
|
116
|
+
|
|
117
|
+
if "data" in data:
|
|
118
|
+
return data["data"]
|
|
119
|
+
return []
|
|
120
|
+
except Exception as e:
|
|
121
|
+
logger.error(f"Error fetching businesses: {e}")
|
|
122
|
+
return []
|
|
123
|
+
|
|
106
124
|
async def search_records(self, query: str, access_token: str) -> List[str]:
|
|
107
125
|
"""Search Meta Ads data and return matching record IDs
|
|
108
126
|
|
|
@@ -230,6 +248,32 @@ class MetaAdsDataManager:
|
|
|
230
248
|
},
|
|
231
249
|
"raw_data": page
|
|
232
250
|
}
|
|
251
|
+
|
|
252
|
+
# If query specifically mentions "business" or "businesses", also search businesses
|
|
253
|
+
if any(term in ['business', 'businesses', 'company', 'companies'] for term in query_terms):
|
|
254
|
+
businesses = await self._get_businesses(access_token, limit=25)
|
|
255
|
+
for business in businesses:
|
|
256
|
+
business_text = f"{business.get('name', '')} {business.get('verification_status', '')}".lower()
|
|
257
|
+
|
|
258
|
+
if any(term in business_text for term in query_terms):
|
|
259
|
+
business_record_id = f"business:{business['id']}"
|
|
260
|
+
matching_ids.append(business_record_id)
|
|
261
|
+
|
|
262
|
+
# Cache the business data
|
|
263
|
+
self._cache[business_record_id] = {
|
|
264
|
+
"id": business_record_id,
|
|
265
|
+
"type": "business",
|
|
266
|
+
"title": f"Business: {business.get('name', 'Unnamed Business')}",
|
|
267
|
+
"text": f"Meta Business {business.get('name', 'Unnamed')} (ID: {business.get('id', 'N/A')}) - Created: {business.get('created_time', 'Unknown')}, Verification: {business.get('verification_status', 'Unknown')}",
|
|
268
|
+
"metadata": {
|
|
269
|
+
"business_id": business.get('id'),
|
|
270
|
+
"business_name": business.get('name'),
|
|
271
|
+
"created_time": business.get('created_time'),
|
|
272
|
+
"verification_status": business.get('verification_status'),
|
|
273
|
+
"data_type": "meta_ads_business"
|
|
274
|
+
},
|
|
275
|
+
"raw_data": business
|
|
276
|
+
}
|
|
233
277
|
|
|
234
278
|
except Exception as e:
|
|
235
279
|
logger.error(f"Error during search operation: {e}")
|
|
@@ -273,7 +317,7 @@ async def search(
|
|
|
273
317
|
Search through Meta Ads data and return matching record IDs.
|
|
274
318
|
|
|
275
319
|
This tool is required for OpenAI ChatGPT Deep Research integration.
|
|
276
|
-
It searches across ad accounts, campaigns, ads, and
|
|
320
|
+
It searches across ad accounts, campaigns, ads, pages, and businesses to find relevant records
|
|
277
321
|
based on the provided query.
|
|
278
322
|
|
|
279
323
|
Args:
|
|
@@ -288,6 +332,7 @@ async def search(
|
|
|
288
332
|
search(query="account spending")
|
|
289
333
|
search(query="facebook ads performance")
|
|
290
334
|
search(query="facebook pages")
|
|
335
|
+
search(query="user businesses")
|
|
291
336
|
"""
|
|
292
337
|
if not query:
|
|
293
338
|
return json.dumps({
|
|
@@ -120,7 +120,7 @@ class PipeboardAuthManager:
|
|
|
120
120
|
else:
|
|
121
121
|
logger.info("Pipeboard authentication not enabled. Set PIPEBOARD_API_TOKEN environment variable to enable.")
|
|
122
122
|
self.token_info = None
|
|
123
|
-
|
|
123
|
+
# Note: Token caching is disabled to always fetch fresh tokens from Pipeboard
|
|
124
124
|
|
|
125
125
|
def _get_token_cache_path(self) -> Path:
|
|
126
126
|
"""Get the platform-specific path for token cache file"""
|
|
@@ -320,18 +320,7 @@ class PipeboardAuthManager:
|
|
|
320
320
|
logger.error("Please set PIPEBOARD_API_TOKEN environment variable")
|
|
321
321
|
return None
|
|
322
322
|
|
|
323
|
-
|
|
324
|
-
if not force_refresh and self.token_info and not self.token_info.is_expired():
|
|
325
|
-
logger.debug("Using existing valid token")
|
|
326
|
-
return self.token_info.access_token
|
|
327
|
-
|
|
328
|
-
# If we have a token but it's expired, log that information
|
|
329
|
-
if not force_refresh and self.token_info and self.token_info.is_expired():
|
|
330
|
-
logger.error("TOKEN VALIDATION FAILED: Existing token is expired")
|
|
331
|
-
if self.token_info.expires_at:
|
|
332
|
-
logger.error(f"Token expiration time: {self.token_info.expires_at}")
|
|
333
|
-
|
|
334
|
-
logger.info(f"Getting new token (force_refresh={force_refresh})")
|
|
323
|
+
logger.info("Getting fresh token from Pipeboard (caching disabled)")
|
|
335
324
|
|
|
336
325
|
# If force refresh or no token/expired token, get a new one from Pipeboard
|
|
337
326
|
try:
|
|
@@ -398,8 +387,7 @@ class PipeboardAuthManager:
|
|
|
398
387
|
token_type=data.get("token_type", "bearer")
|
|
399
388
|
)
|
|
400
389
|
|
|
401
|
-
#
|
|
402
|
-
self._save_token_to_cache()
|
|
390
|
+
# Note: Token caching is disabled
|
|
403
391
|
|
|
404
392
|
masked_token = self.token_info.access_token[:10] + "..." + self.token_info.access_token[-5:] if self.token_info.access_token else "None"
|
|
405
393
|
logger.info(f"Successfully retrieved access token: {masked_token}")
|
|
@@ -14,7 +14,7 @@ from .pipeboard_auth import pipeboard_auth_manager
|
|
|
14
14
|
import time
|
|
15
15
|
|
|
16
16
|
# Initialize FastMCP server
|
|
17
|
-
mcp_server = FastMCP("meta-ads"
|
|
17
|
+
mcp_server = FastMCP("meta-ads")
|
|
18
18
|
|
|
19
19
|
# Register resource URIs
|
|
20
20
|
mcp_server.resource(uri="meta-ads://resources")(list_resources)
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "meta-ads-mcp"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.9.1"
|
|
8
8
|
description = "Model Context Protocol (MCP) plugin for interacting with Meta Ads API"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -20,7 +20,7 @@ classifiers = [
|
|
|
20
20
|
]
|
|
21
21
|
dependencies = [
|
|
22
22
|
"httpx>=0.26.0",
|
|
23
|
-
"mcp[cli]>=1.10.1",
|
|
23
|
+
"mcp[cli]>=1.10.1,<=1.12.2",
|
|
24
24
|
"python-dotenv>=1.1.0",
|
|
25
25
|
"requests>=2.32.3",
|
|
26
26
|
"Pillow>=10.0.0",
|