meta-ads-mcp 0.11.0__tar.gz → 0.11.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/PKG-INFO +1 -1
  2. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/meta_ads_mcp/__init__.py +1 -1
  3. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/ads.py +118 -113
  4. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/pyproject.toml +1 -1
  5. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/.github/workflows/publish.yml +0 -0
  6. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/.github/workflows/test.yml +0 -0
  7. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/.gitignore +0 -0
  8. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/CUSTOM_META_APP.md +0 -0
  9. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/Dockerfile +0 -0
  10. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/LICENSE +0 -0
  11. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/LOCAL_INSTALLATION.md +0 -0
  12. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/META_API_NOTES.md +0 -0
  13. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/README.md +0 -0
  14. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/RELEASE.md +0 -0
  15. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/STREAMABLE_HTTP_SETUP.md +0 -0
  16. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/examples/README.md +0 -0
  17. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/examples/example_http_client.py +0 -0
  18. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/future_improvements.md +0 -0
  19. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/images/meta-ads-example.png +0 -0
  20. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/meta_ads_auth.sh +0 -0
  21. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/meta_ads_mcp/__main__.py +0 -0
  22. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/__init__.py +0 -0
  23. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/accounts.py +0 -0
  24. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/ads_library.py +0 -0
  25. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/adsets.py +0 -0
  26. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/api.py +0 -0
  27. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/auth.py +0 -0
  28. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/authentication.py +0 -0
  29. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/budget_schedules.py +0 -0
  30. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/callback_server.py +0 -0
  31. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/campaigns.py +0 -0
  32. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/duplication.py +0 -0
  33. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/http_auth_integration.py +0 -0
  34. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/insights.py +0 -0
  35. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/openai_deep_research.py +0 -0
  36. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/pipeboard_auth.py +0 -0
  37. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/reports.py +0 -0
  38. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/resources.py +0 -0
  39. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/server.py +0 -0
  40. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/targeting.py +0 -0
  41. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/utils.py +0 -0
  42. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/requirements.txt +0 -0
  43. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/setup.py +0 -0
  44. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/smithery.yaml +0 -0
  45. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/tests/README.md +0 -0
  46. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/tests/README_REGRESSION_TESTS.md +0 -0
  47. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/tests/__init__.py +0 -0
  48. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/tests/conftest.py +0 -0
  49. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/tests/e2e_account_info_search_issue.py +0 -0
  50. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/tests/test_account_info_access_fix.py +0 -0
  51. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/tests/test_account_search.py +0 -0
  52. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/tests/test_budget_update.py +0 -0
  53. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/tests/test_budget_update_e2e.py +0 -0
  54. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/tests/test_dsa_beneficiary.py +0 -0
  55. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/tests/test_dsa_integration.py +0 -0
  56. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/tests/test_duplication.py +0 -0
  57. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/tests/test_duplication_regression.py +0 -0
  58. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/tests/test_dynamic_creatives.py +0 -0
  59. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/tests/test_estimate_audience_size.py +0 -0
  60. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/tests/test_estimate_audience_size_e2e.py +0 -0
  61. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/tests/test_get_account_pages.py +0 -0
  62. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/tests/test_get_ad_creatives_fix.py +0 -0
  63. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/tests/test_get_ad_image_quality_improvements.py +0 -0
  64. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/tests/test_get_ad_image_regression.py +0 -0
  65. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/tests/test_http_transport.py +0 -0
  66. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/tests/test_insights_actions_and_values.py +0 -0
  67. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/tests/test_integration_openai_mcp.py +0 -0
  68. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/tests/test_mobile_app_adset_creation.py +0 -0
  69. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/tests/test_mobile_app_adset_issue.py +0 -0
  70. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/tests/test_openai.py +0 -0
  71. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/tests/test_openai_mcp_deep_research.py +0 -0
  72. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/tests/test_page_discovery.py +0 -0
  73. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/tests/test_page_discovery_integration.py +0 -0
  74. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/tests/test_targeting.py +0 -0
  75. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/tests/test_targeting_search_e2e.py +0 -0
  76. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/tests/test_update_ad_creative_id.py +0 -0
  77. {meta_ads_mcp-0.11.0 → meta_ads_mcp-0.11.1}/tests/test_upload_ad_image.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meta-ads-mcp
3
- Version: 0.11.0
3
+ Version: 0.11.1
4
4
  Summary: Model Context Protocol (MCP) server for interacting with Meta Ads API
5
5
  Project-URL: Homepage, https://github.com/pipeboard-co/meta-ads-mcp
6
6
  Project-URL: Bug Tracker, https://github.com/pipeboard-co/meta-ads-mcp/issues
@@ -6,7 +6,7 @@ This package provides a Meta Ads MCP integration
6
6
 
7
7
  from meta_ads_mcp.core.server import main
8
8
 
9
- __version__ = "0.11.0"
9
+ __version__ = "0.11.1"
10
10
 
11
11
  __all__ = [
12
12
  'get_ad_accounts',
@@ -14,6 +14,10 @@ from .utils import download_image, try_multiple_download_methods, ad_creative_im
14
14
  from .server import mcp_server
15
15
 
16
16
 
17
+ # Only register the save_ad_image_locally function if explicitly enabled via environment variable
18
+ ENABLE_SAVE_AD_IMAGE_LOCALLY = bool(os.environ.get("META_ADS_ENABLE_SAVE_AD_IMAGE_LOCALLY", ""))
19
+
20
+
17
21
  @mcp_server.tool()
18
22
  @meta_api_tool
19
23
  async def get_ads(account_id: str, access_token: Optional[str] = None, limit: int = 10,
@@ -380,130 +384,131 @@ async def get_ad_image(ad_id: str, access_token: Optional[str] = None) -> Image:
380
384
  return f"Error processing image: {str(e)}"
381
385
 
382
386
 
383
- @mcp_server.tool()
384
- @meta_api_tool
385
- async def save_ad_image_locally(ad_id: str, access_token: Optional[str] = None, output_dir: str = "ad_images") -> str:
386
- """
387
- Get, download, and save a Meta ad image locally, returning the file path.
388
-
389
- Args:
390
- ad_id: Meta Ads ad ID
391
- access_token: Meta API access token (optional - will use cached token if not provided)
392
- output_dir: Directory to save the image file (default: 'ad_images')
393
-
394
- Returns:
395
- The file path to the saved image, or an error message string.
396
- """
397
- if not ad_id:
398
- return json.dumps({"error": "No ad ID provided"}, indent=2)
387
+ if ENABLE_SAVE_AD_IMAGE_LOCALLY:
388
+ @mcp_server.tool()
389
+ @meta_api_tool
390
+ async def save_ad_image_locally(ad_id: str, access_token: Optional[str] = None, output_dir: str = "ad_images") -> str:
391
+ """
392
+ Get, download, and save a Meta ad image locally, returning the file path.
399
393
 
400
- print(f"Attempting to get and save creative image for ad {ad_id}")
401
-
402
- # First, get creative and account IDs
403
- ad_endpoint = f"{ad_id}"
404
- ad_params = {
405
- "fields": "creative{id},account_id"
406
- }
407
-
408
- ad_data = await make_api_request(ad_endpoint, access_token, ad_params)
409
-
410
- if "error" in ad_data:
411
- return json.dumps({"error": f"Could not get ad data - {json.dumps(ad_data)}"}, indent=2)
412
-
413
- account_id = ad_data.get("account_id")
414
- if not account_id:
415
- return json.dumps({"error": "No account ID found for ad"}, indent=2)
416
-
417
- if "creative" not in ad_data:
418
- return json.dumps({"error": "No creative found for this ad"}, indent=2)
394
+ Args:
395
+ ad_id: Meta Ads ad ID
396
+ access_token: Meta API access token (optional - will use cached token if not provided)
397
+ output_dir: Directory to save the image file (default: 'ad_images')
419
398
 
420
- creative_data = ad_data.get("creative", {})
421
- creative_id = creative_data.get("id")
422
- if not creative_id:
423
- return json.dumps({"error": "No creative ID found"}, indent=2)
424
-
425
- # Get creative details to find image hash
426
- creative_endpoint = f"{creative_id}"
427
- creative_params = {
428
- "fields": "id,name,image_hash,asset_feed_spec"
429
- }
430
- creative_details = await make_api_request(creative_endpoint, access_token, creative_params)
431
-
432
- image_hashes = []
433
- if "image_hash" in creative_details:
434
- image_hashes.append(creative_details["image_hash"])
435
- if "asset_feed_spec" in creative_details and "images" in creative_details["asset_feed_spec"]:
436
- for image in creative_details["asset_feed_spec"]["images"]:
437
- if "hash" in image:
438
- image_hashes.append(image["hash"])
439
-
440
- if not image_hashes:
441
- # Fallback attempt (as in get_ad_image)
442
- creative_json = await get_ad_creatives(ad_id=ad_id, access_token=access_token) # Ensure ad_id is passed correctly
443
- creative_data_list = json.loads(creative_json)
444
- if 'data' in creative_data_list and creative_data_list['data']:
445
- first_creative = creative_data_list['data'][0]
446
- if 'object_story_spec' in first_creative and 'link_data' in first_creative['object_story_spec'] and 'image_hash' in first_creative['object_story_spec']['link_data']:
447
- image_hashes.append(first_creative['object_story_spec']['link_data']['image_hash'])
448
- elif 'image_hash' in first_creative: # Check direct hash on creative data
449
- image_hashes.append(first_creative['image_hash'])
399
+ Returns:
400
+ The file path to the saved image, or an error message string.
401
+ """
402
+ if not ad_id:
403
+ return json.dumps({"error": "No ad ID provided"}, indent=2)
404
+
405
+ print(f"Attempting to get and save creative image for ad {ad_id}")
406
+
407
+ # First, get creative and account IDs
408
+ ad_endpoint = f"{ad_id}"
409
+ ad_params = {
410
+ "fields": "creative{id},account_id"
411
+ }
412
+
413
+ ad_data = await make_api_request(ad_endpoint, access_token, ad_params)
414
+
415
+ if "error" in ad_data:
416
+ return json.dumps({"error": f"Could not get ad data - {json.dumps(ad_data)}"}, indent=2)
417
+
418
+ account_id = ad_data.get("account_id")
419
+ if not account_id:
420
+ return json.dumps({"error": "No account ID found for ad"}, indent=2)
421
+
422
+ if "creative" not in ad_data:
423
+ return json.dumps({"error": "No creative found for this ad"}, indent=2)
424
+
425
+ creative_data = ad_data.get("creative", {})
426
+ creative_id = creative_data.get("id")
427
+ if not creative_id:
428
+ return json.dumps({"error": "No creative ID found"}, indent=2)
429
+
430
+ # Get creative details to find image hash
431
+ creative_endpoint = f"{creative_id}"
432
+ creative_params = {
433
+ "fields": "id,name,image_hash,asset_feed_spec"
434
+ }
435
+ creative_details = await make_api_request(creative_endpoint, access_token, creative_params)
436
+
437
+ image_hashes = []
438
+ if "image_hash" in creative_details:
439
+ image_hashes.append(creative_details["image_hash"])
440
+ if "asset_feed_spec" in creative_details and "images" in creative_details["asset_feed_spec"]:
441
+ for image in creative_details["asset_feed_spec"]["images"]:
442
+ if "hash" in image:
443
+ image_hashes.append(image["hash"])
444
+
445
+ if not image_hashes:
446
+ # Fallback attempt (as in get_ad_image)
447
+ creative_json = await get_ad_creatives(ad_id=ad_id, access_token=access_token) # Ensure ad_id is passed correctly
448
+ creative_data_list = json.loads(creative_json)
449
+ if 'data' in creative_data_list and creative_data_list['data']:
450
+ first_creative = creative_data_list['data'][0]
451
+ if 'object_story_spec' in first_creative and 'link_data' in first_creative['object_story_spec'] and 'image_hash' in first_creative['object_story_spec']['link_data']:
452
+ image_hashes.append(first_creative['object_story_spec']['link_data']['image_hash'])
453
+ elif 'image_hash' in first_creative: # Check direct hash on creative data
454
+ image_hashes.append(first_creative['image_hash'])
450
455
 
451
456
 
452
- if not image_hashes:
453
- return json.dumps({"error": "No image hashes found in creative or fallback"}, indent=2)
457
+ if not image_hashes:
458
+ return json.dumps({"error": "No image hashes found in creative or fallback"}, indent=2)
454
459
 
455
- print(f"Found image hashes: {image_hashes}")
456
-
457
- # Fetch image data using the first hash
458
- image_endpoint = f"act_{account_id}/adimages"
459
- hashes_str = f'["{image_hashes[0]}"]'
460
- image_params = {
461
- "fields": "hash,url,width,height,name,status",
462
- "hashes": hashes_str
463
- }
464
-
465
- print(f"Requesting image data with params: {image_params}")
466
- image_data = await make_api_request(image_endpoint, access_token, image_params)
467
-
468
- if "error" in image_data:
469
- return json.dumps({"error": f"Failed to get image data - {json.dumps(image_data)}"}, indent=2)
470
-
471
- if "data" not in image_data or not image_data["data"]:
472
- return json.dumps({"error": "No image data returned from API"}, indent=2)
460
+ print(f"Found image hashes: {image_hashes}")
473
461
 
474
- first_image = image_data["data"][0]
475
- image_url = first_image.get("url")
476
-
477
- if not image_url:
478
- return json.dumps({"error": "No valid image URL found in API response"}, indent=2)
462
+ # Fetch image data using the first hash
463
+ image_endpoint = f"act_{account_id}/adimages"
464
+ hashes_str = f'["{image_hashes[0]}"]'
465
+ image_params = {
466
+ "fields": "hash,url,width,height,name,status",
467
+ "hashes": hashes_str
468
+ }
479
469
 
480
- print(f"Downloading image from URL: {image_url}")
481
-
482
- # Download and Save Image
483
- image_bytes = await download_image(image_url)
484
-
485
- if not image_bytes:
486
- return json.dumps({"error": "Failed to download image"}, indent=2)
470
+ print(f"Requesting image data with params: {image_params}")
471
+ image_data = await make_api_request(image_endpoint, access_token, image_params)
487
472
 
488
- try:
489
- # Ensure output directory exists
490
- if not os.path.exists(output_dir):
491
- os.makedirs(output_dir)
473
+ if "error" in image_data:
474
+ return json.dumps({"error": f"Failed to get image data - {json.dumps(image_data)}"}, indent=2)
475
+
476
+ if "data" not in image_data or not image_data["data"]:
477
+ return json.dumps({"error": "No image data returned from API"}, indent=2)
478
+
479
+ first_image = image_data["data"][0]
480
+ image_url = first_image.get("url")
481
+
482
+ if not image_url:
483
+ return json.dumps({"error": "No valid image URL found in API response"}, indent=2)
484
+
485
+ print(f"Downloading image from URL: {image_url}")
486
+
487
+ # Download and Save Image
488
+ image_bytes = await download_image(image_url)
489
+
490
+ if not image_bytes:
491
+ return json.dumps({"error": "Failed to download image"}, indent=2)
492
492
 
493
- # Create a filename (e.g., using ad_id and image hash)
494
- file_extension = ".jpg" # Default extension, could try to infer from headers later
495
- filename = f"{ad_id}_{image_hashes[0]}{file_extension}"
496
- filepath = os.path.join(output_dir, filename)
497
-
498
- # Save the image bytes to the file
499
- with open(filepath, "wb") as f:
500
- f.write(image_bytes)
493
+ try:
494
+ # Ensure output directory exists
495
+ if not os.path.exists(output_dir):
496
+ os.makedirs(output_dir)
497
+
498
+ # Create a filename (e.g., using ad_id and image hash)
499
+ file_extension = ".jpg" # Default extension, could try to infer from headers later
500
+ filename = f"{ad_id}_{image_hashes[0]}{file_extension}"
501
+ filepath = os.path.join(output_dir, filename)
501
502
 
502
- print(f"Image saved successfully to: {filepath}")
503
- return json.dumps({"filepath": filepath}, indent=2) # Return JSON with filepath
503
+ # Save the image bytes to the file
504
+ with open(filepath, "wb") as f:
505
+ f.write(image_bytes)
506
+
507
+ print(f"Image saved successfully to: {filepath}")
508
+ return json.dumps({"filepath": filepath}, indent=2) # Return JSON with filepath
504
509
 
505
- except Exception as e:
506
- return json.dumps({"error": f"Failed to save image: {str(e)}"}, indent=2)
510
+ except Exception as e:
511
+ return json.dumps({"error": f"Failed to save image: {str(e)}"}, indent=2)
507
512
 
508
513
 
509
514
  @mcp_server.tool()
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "meta-ads-mcp"
7
- version = "0.11.0"
7
+ version = "0.11.1"
8
8
  description = "Model Context Protocol (MCP) server for interacting with Meta Ads API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes