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.
Files changed (71) hide show
  1. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/PKG-INFO +2 -2
  2. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/__init__.py +1 -1
  3. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/ads.py +160 -127
  4. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/api.py +36 -15
  5. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/insights.py +23 -1
  6. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/openai_deep_research.py +46 -1
  7. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/pipeboard_auth.py +3 -15
  8. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/server.py +1 -1
  9. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/pyproject.toml +2 -2
  10. meta_ads_mcp-0.9.1/tests/test_get_account_pages.py +399 -0
  11. meta_ads_mcp-0.9.1/tests/test_openai.py +29 -0
  12. meta_ads_mcp-0.9.1/tests/test_update_ad_creative_id.py +339 -0
  13. meta_ads_mcp-0.8.0/tests/test_openai.py +0 -23
  14. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/.github/workflows/publish.yml +0 -0
  15. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/.github/workflows/test.yml +0 -0
  16. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/.gitignore +0 -0
  17. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/CUSTOM_META_APP.md +0 -0
  18. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/Dockerfile +0 -0
  19. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/LICENSE +0 -0
  20. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/LOCAL_INSTALLATION.md +0 -0
  21. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/META_API_NOTES.md +0 -0
  22. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/README.md +0 -0
  23. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/RELEASE.md +0 -0
  24. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/STREAMABLE_HTTP_SETUP.md +0 -0
  25. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/examples/README.md +0 -0
  26. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/examples/example_http_client.py +0 -0
  27. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/future_improvements.md +0 -0
  28. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/images/meta-ads-example.png +0 -0
  29. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_auth.sh +0 -0
  30. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/__main__.py +0 -0
  31. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/__init__.py +0 -0
  32. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/accounts.py +0 -0
  33. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/ads_library.py +0 -0
  34. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/adsets.py +0 -0
  35. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/auth.py +0 -0
  36. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/authentication.py +0 -0
  37. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/budget_schedules.py +0 -0
  38. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/callback_server.py +0 -0
  39. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/campaigns.py +0 -0
  40. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/duplication.py +0 -0
  41. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/http_auth_integration.py +0 -0
  42. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/reports.py +0 -0
  43. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/resources.py +0 -0
  44. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/targeting.py +0 -0
  45. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/meta_ads_mcp/core/utils.py +0 -0
  46. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/requirements.txt +0 -0
  47. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/setup.py +0 -0
  48. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/smithery.yaml +0 -0
  49. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/README.md +0 -0
  50. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/README_REGRESSION_TESTS.md +0 -0
  51. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/__init__.py +0 -0
  52. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/conftest.py +0 -0
  53. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/test_account_search.py +0 -0
  54. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/test_budget_update.py +0 -0
  55. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/test_budget_update_e2e.py +0 -0
  56. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/test_dsa_beneficiary.py +0 -0
  57. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/test_dsa_integration.py +0 -0
  58. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/test_duplication.py +0 -0
  59. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/test_duplication_regression.py +0 -0
  60. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/test_dynamic_creatives.py +0 -0
  61. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/test_get_ad_creatives_fix.py +0 -0
  62. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/test_get_ad_image_quality_improvements.py +0 -0
  63. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/test_get_ad_image_regression.py +0 -0
  64. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/test_http_transport.py +0 -0
  65. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/test_insights_actions_and_values.py +0 -0
  66. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/test_integration_openai_mcp.py +0 -0
  67. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/test_openai_mcp_deep_research.py +0 -0
  68. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/test_page_discovery.py +0 -0
  69. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/test_page_discovery_integration.py +0 -0
  70. {meta_ads_mcp-0.8.0 → meta_ads_mcp-0.9.1}/tests/test_targeting.py +0 -0
  71. {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.8.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]>=1.10.1
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
@@ -7,7 +7,7 @@ with the Claude LLM.
7
7
 
8
8
  from meta_ads_mcp.core.server import main
9
9
 
10
- __version__ = "0.8.0"
10
+ __version__ = "0.9.1"
11
11
 
12
12
  __all__ = [
13
13
  'get_ad_accounts',
@@ -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 tracking_specs)"}, indent=2)
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
- data = await make_api_request(endpoint, access_token, params, method='POST')
551
-
552
- return json.dumps(data, indent=2)
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
- # Try all approaches that might work
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
- # If we found page IDs, get details for each
1227
- if page_ids:
1228
- page_details = {"data": []}
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
- for page_id in page_ids:
1231
- page_endpoint = f"{page_id}"
1232
- page_params = {
1233
- "fields": "id,name,username,category,fan_count,link,verification_status,picture"
1234
- }
1235
-
1236
- page_data = await make_api_request(page_endpoint, access_token, page_params)
1237
- if "id" in page_data:
1238
- page_details["data"].append(page_data)
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
- if page_details["data"]:
1241
- return json.dumps(page_details, indent=2)
1242
-
1243
- # Approach 2: Try client_pages endpoint
1244
- endpoint = f"{account_id}/client_pages"
1245
- params = {
1246
- "fields": "id,name,username,category,fan_count,link,verification_status,picture"
1247
- }
1248
-
1249
- client_pages_data = await make_api_request(endpoint, access_token, params)
1250
-
1251
- if "data" in client_pages_data and client_pages_data["data"]:
1252
- return json.dumps(client_pages_data, indent=2)
1253
-
1254
- # Approach 3: Try promoted_objects endpoint to find page IDs
1255
- endpoint = f"{account_id}/promoted_objects"
1256
- params = {
1257
- "fields": "page_id"
1258
- }
1259
-
1260
- promoted_objects_data = await make_api_request(endpoint, access_token, params)
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
- if page_ids:
1269
- page_details = {"data": []}
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
- if page_details["data"]:
1281
- return json.dumps(page_details, indent=2)
1282
-
1283
- # Approach 4: Extract page IDs from tracking_specs in ads
1284
- # Inspired by praveen92y's implementation for robust page detection
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": "Page details not found",
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 using automated methods",
1344
- "troubleshooting": {
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
- return json.dumps({
244
- "error": {
245
- "message": "Authentication Required",
246
- "details": {
247
- "description": "You need to authenticate with the Meta API before using this tool",
248
- "action_required": "Please authenticate first",
249
- "auth_url": auth_url,
250
- "configuration_status": {
251
- "app_id_configured": bool(app_id) and app_id != "YOUR_META_APP_ID",
252
- "pipeboard_enabled": bool(os.environ.get('PIPEBOARD_API_TOKEN')),
253
- },
254
- "troubleshooting": "Check logs for TOKEN VALIDATION FAILED messages",
255
- "markdown_link": f"[Click here to authenticate with Meta Ads API]({auth_url})"
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 (e.g., age, gender, country)
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 pages to find relevant records
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
- self._load_cached_token()
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
- # Check if we already have a valid token
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
- # Save to cache
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", use_consistent_tool_format=True)
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.8.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",