meta-ads-mcp 0.10.2__tar.gz → 0.10.6__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 (76) hide show
  1. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/LOCAL_INSTALLATION.md +1 -1
  2. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/PKG-INFO +2 -2
  3. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/README.md +1 -1
  4. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/meta_ads_mcp/__init__.py +2 -3
  5. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/meta_ads_mcp/core/__init__.py +2 -2
  6. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/meta_ads_mcp/core/ads.py +77 -26
  7. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/meta_ads_mcp/core/authentication.py +9 -2
  8. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/pyproject.toml +1 -1
  9. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/.github/workflows/publish.yml +0 -0
  10. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/.github/workflows/test.yml +0 -0
  11. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/.gitignore +0 -0
  12. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/CUSTOM_META_APP.md +0 -0
  13. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/Dockerfile +0 -0
  14. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/LICENSE +0 -0
  15. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/META_API_NOTES.md +0 -0
  16. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/RELEASE.md +0 -0
  17. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/STREAMABLE_HTTP_SETUP.md +0 -0
  18. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/examples/README.md +0 -0
  19. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/examples/example_http_client.py +0 -0
  20. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/future_improvements.md +0 -0
  21. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/images/meta-ads-example.png +0 -0
  22. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/meta_ads_auth.sh +0 -0
  23. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/meta_ads_mcp/__main__.py +0 -0
  24. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/meta_ads_mcp/core/accounts.py +0 -0
  25. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/meta_ads_mcp/core/ads_library.py +0 -0
  26. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/meta_ads_mcp/core/adsets.py +0 -0
  27. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/meta_ads_mcp/core/api.py +0 -0
  28. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/meta_ads_mcp/core/auth.py +0 -0
  29. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/meta_ads_mcp/core/budget_schedules.py +0 -0
  30. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/meta_ads_mcp/core/callback_server.py +0 -0
  31. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/meta_ads_mcp/core/campaigns.py +0 -0
  32. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/meta_ads_mcp/core/duplication.py +0 -0
  33. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/meta_ads_mcp/core/http_auth_integration.py +0 -0
  34. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/meta_ads_mcp/core/insights.py +0 -0
  35. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/meta_ads_mcp/core/openai_deep_research.py +0 -0
  36. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/meta_ads_mcp/core/pipeboard_auth.py +0 -0
  37. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/meta_ads_mcp/core/reports.py +0 -0
  38. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/meta_ads_mcp/core/resources.py +0 -0
  39. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/meta_ads_mcp/core/server.py +0 -0
  40. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/meta_ads_mcp/core/targeting.py +0 -0
  41. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/meta_ads_mcp/core/utils.py +0 -0
  42. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/requirements.txt +0 -0
  43. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/setup.py +0 -0
  44. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/smithery.yaml +0 -0
  45. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/tests/README.md +0 -0
  46. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/tests/README_REGRESSION_TESTS.md +0 -0
  47. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/tests/__init__.py +0 -0
  48. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/tests/conftest.py +0 -0
  49. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/tests/e2e_account_info_search_issue.py +0 -0
  50. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/tests/test_account_info_access_fix.py +0 -0
  51. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/tests/test_account_search.py +0 -0
  52. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/tests/test_budget_update.py +0 -0
  53. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/tests/test_budget_update_e2e.py +0 -0
  54. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/tests/test_dsa_beneficiary.py +0 -0
  55. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/tests/test_dsa_integration.py +0 -0
  56. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/tests/test_duplication.py +0 -0
  57. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/tests/test_duplication_regression.py +0 -0
  58. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/tests/test_dynamic_creatives.py +0 -0
  59. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/tests/test_estimate_audience_size.py +0 -0
  60. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/tests/test_estimate_audience_size_e2e.py +0 -0
  61. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/tests/test_get_account_pages.py +0 -0
  62. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/tests/test_get_ad_creatives_fix.py +0 -0
  63. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/tests/test_get_ad_image_quality_improvements.py +0 -0
  64. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/tests/test_get_ad_image_regression.py +0 -0
  65. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/tests/test_http_transport.py +0 -0
  66. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/tests/test_insights_actions_and_values.py +0 -0
  67. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/tests/test_integration_openai_mcp.py +0 -0
  68. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/tests/test_mobile_app_adset_creation.py +0 -0
  69. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/tests/test_mobile_app_adset_issue.py +0 -0
  70. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/tests/test_openai.py +0 -0
  71. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/tests/test_openai_mcp_deep_research.py +0 -0
  72. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/tests/test_page_discovery.py +0 -0
  73. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/tests/test_page_discovery_integration.py +0 -0
  74. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/tests/test_targeting.py +0 -0
  75. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/tests/test_targeting_search_e2e.py +0 -0
  76. {meta_ads_mcp-0.10.2 → meta_ads_mcp-0.10.6}/tests/test_update_ad_creative_id.py +0 -0
@@ -480,7 +480,7 @@ If you're still experiencing issues:
480
480
 
481
481
  1. **Check the logs** for detailed error messages
482
482
  2. **Search existing issues** on GitHub
483
- 3. **Join our Discord** at [discord.gg/hNxpJcqM52](https://discord.gg/hNxpJcqM52)
483
+ 3. **Join our Discord** at [discord.gg/YNjF7gUb5h](https://discord.gg/YNjF7gUb5h)
484
484
  4. **Email support** at info@pipeboard.co
485
485
  5. **Consider Remote MCP** at [pipeboard.co](https://pipeboard.co) as an alternative
486
486
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meta-ads-mcp
3
- Version: 0.10.2
3
+ Version: 0.10.6
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
@@ -33,7 +33,7 @@ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server for in
33
33
 
34
34
  ## Community & Support
35
35
 
36
- - [Discord](https://discord.gg/hNxpJcqM52). Join the community.
36
+ - [Discord](https://discord.gg/YNjF7gUb5h). Join the community.
37
37
  - [Email Support](info@pipeboard.co). Email us for support.
38
38
 
39
39
  ## Table of Contents
@@ -8,7 +8,7 @@ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server for in
8
8
 
9
9
  ## Community & Support
10
10
 
11
- - [Discord](https://discord.gg/hNxpJcqM52). Join the community.
11
+ - [Discord](https://discord.gg/YNjF7gUb5h). Join the community.
12
12
  - [Email Support](info@pipeboard.co). Email us for support.
13
13
 
14
14
  ## Table of Contents
@@ -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.10.2"
10
+ __version__ = "0.10.6"
11
11
 
12
12
  __all__ = [
13
13
  'get_ad_accounts',
@@ -24,7 +24,7 @@ __all__ = [
24
24
  'get_ad_image',
25
25
  'update_ad',
26
26
  'get_insights',
27
- 'get_login_link',
27
+ # 'get_login_link' is conditionally exported via core.__all__
28
28
  'login_cli',
29
29
  'main',
30
30
  'search_interests',
@@ -51,7 +51,6 @@ from .core import (
51
51
  get_ad_image,
52
52
  update_ad,
53
53
  get_insights,
54
- get_login_link,
55
54
  login_cli,
56
55
  main,
57
56
  search_interests,
@@ -6,7 +6,7 @@ from .campaigns import get_campaigns, get_campaign_details, create_campaign
6
6
  from .adsets import get_adsets, get_adset_details, update_adset
7
7
  from .ads import get_ads, get_ad_details, get_ad_creatives, get_ad_image, update_ad
8
8
  from .insights import get_insights
9
- from .authentication import get_login_link
9
+ from . import authentication # Import module to register conditional auth tools
10
10
  from .server import login_cli, main
11
11
  from .auth import login
12
12
  from . import ads_library # Import module to register conditional tools
@@ -32,7 +32,7 @@ __all__ = [
32
32
  'get_ad_image',
33
33
  'update_ad',
34
34
  'get_insights',
35
- 'get_login_link',
35
+ # Note: 'get_login_link' is registered conditionally by the authentication module
36
36
  'login_cli',
37
37
  'login',
38
38
  'main',
@@ -564,7 +564,8 @@ async def update_ad(
564
564
  async def upload_ad_image(
565
565
  access_token: str = None,
566
566
  account_id: str = None,
567
- image_path: str = None,
567
+ file: str = None,
568
+ image_url: str = None,
568
569
  name: str = None
569
570
  ) -> str:
570
571
  """
@@ -573,7 +574,8 @@ async def upload_ad_image(
573
574
  Args:
574
575
  access_token: Meta API access token (optional - will use cached token if not provided)
575
576
  account_id: Meta Ads account ID (format: act_XXXXXXXXX)
576
- image_path: Path to the image file to upload
577
+ file: Data URL or raw base64 string of the image (e.g., "data:image/png;base64,iVBORw0KG...")
578
+ image_url: Direct URL to an image to fetch and upload
577
579
  name: Optional name for the image (default: filename)
578
580
 
579
581
  Returns:
@@ -583,45 +585,94 @@ async def upload_ad_image(
583
585
  if not account_id:
584
586
  return json.dumps({"error": "No account ID provided"}, indent=2)
585
587
 
586
- if not image_path:
587
- return json.dumps({"error": "No image path provided"}, indent=2)
588
+ # Ensure we have image data
589
+ if not file and not image_url:
590
+ return json.dumps({"error": "Provide either 'file' (data URL or base64) or 'image_url'"}, indent=2)
588
591
 
589
592
  # Ensure account_id has the 'act_' prefix for API compatibility
590
593
  if not account_id.startswith("act_"):
591
594
  account_id = f"act_{account_id}"
592
595
 
593
- # Check if image file exists
594
- if not os.path.exists(image_path):
595
- return json.dumps({"error": f"Image file not found: {image_path}"}, indent=2)
596
-
597
596
  try:
598
- # Read image file
599
- with open(image_path, "rb") as img_file:
600
- image_bytes = img_file.read()
601
-
602
- # Get image filename if name not provided
603
- if not name:
604
- name = os.path.basename(image_path)
605
-
597
+ # Determine encoded_image (base64 string without data URL prefix) and a sensible name
598
+ encoded_image: str = ""
599
+ inferred_name: str = name or ""
600
+
601
+ if file:
602
+ # Support data URL (e.g., data:image/png;base64,...) and raw base64
603
+ data_url_prefix = "data:"
604
+ base64_marker = "base64,"
605
+ if file.startswith(data_url_prefix) and base64_marker in file:
606
+ header, base64_payload = file.split(base64_marker, 1)
607
+ encoded_image = base64_payload.strip()
608
+
609
+ # Infer file extension from MIME type if name not provided
610
+ if not inferred_name:
611
+ # Example header: data:image/png;...
612
+ mime_type = header[len(data_url_prefix):].split(";")[0].strip()
613
+ extension_map = {
614
+ "image/png": ".png",
615
+ "image/jpeg": ".jpg",
616
+ "image/jpg": ".jpg",
617
+ "image/webp": ".webp",
618
+ "image/gif": ".gif",
619
+ "image/bmp": ".bmp",
620
+ "image/tiff": ".tiff",
621
+ }
622
+ ext = extension_map.get(mime_type, ".png")
623
+ inferred_name = f"upload{ext}"
624
+ else:
625
+ # Assume it's already raw base64
626
+ encoded_image = file.strip()
627
+ if not inferred_name:
628
+ inferred_name = "upload.png"
629
+ else:
630
+ # Download image from URL
631
+ try:
632
+ image_bytes = await try_multiple_download_methods(image_url)
633
+ except Exception as download_error:
634
+ return json.dumps({
635
+ "error": "Failed to download image from URL",
636
+ "details": str(download_error),
637
+ "image_url": image_url,
638
+ }, indent=2)
639
+
640
+ if not image_bytes:
641
+ return json.dumps({
642
+ "error": "No data returned when downloading image from URL",
643
+ "image_url": image_url,
644
+ }, indent=2)
645
+
646
+ import base64 # Local import
647
+ encoded_image = base64.b64encode(image_bytes).decode("utf-8")
648
+
649
+ # Infer name from URL if not provided
650
+ if not inferred_name:
651
+ try:
652
+ path_no_query = image_url.split("?")[0]
653
+ filename_from_url = os.path.basename(path_no_query)
654
+ inferred_name = filename_from_url if filename_from_url else "upload.jpg"
655
+ except Exception:
656
+ inferred_name = "upload.jpg"
657
+
658
+ # Final name resolution
659
+ final_name = name or inferred_name or "upload.png"
660
+
606
661
  # Prepare the API endpoint for uploading images
607
662
  endpoint = f"{account_id}/adimages"
608
-
609
- # We need to convert the binary data to base64 for API upload
610
- import base64
611
- encoded_image = base64.b64encode(image_bytes).decode('utf-8')
612
-
613
- # Prepare POST parameters
663
+
664
+ # Prepare POST parameters expected by Meta API
614
665
  params = {
615
666
  "bytes": encoded_image,
616
- "name": name
667
+ "name": final_name,
617
668
  }
618
-
669
+
619
670
  # Make API request to upload the image
620
671
  print(f"Uploading image to Facebook Ad Account {account_id}")
621
672
  data = await make_api_request(endpoint, access_token, params, method="POST")
622
-
673
+
623
674
  return json.dumps(data, indent=2)
624
-
675
+
625
676
  except Exception as e:
626
677
  return json.dumps({
627
678
  "error": "Failed to upload image",
@@ -21,6 +21,7 @@ Environment Variables:
21
21
  - PIPEBOARD_API_TOKEN: Enables mode 2 (token-based auth)
22
22
  - META_ADS_DISABLE_CALLBACK_SERVER: Disables local server, enables mode 3
23
23
  - META_ACCESS_TOKEN: Direct Meta token (fallback)
24
+ - META_ADS_DISABLE_LOGIN_LINK: Hard-disables the get_login_link tool; returns a disabled message
24
25
  """
25
26
 
26
27
  import json
@@ -32,8 +33,10 @@ from .server import mcp_server
32
33
  from .utils import logger, META_APP_SECRET
33
34
  from .pipeboard_auth import pipeboard_auth_manager
34
35
 
36
+ # Only register the login link tool if not explicitly disabled
37
+ ENABLE_LOGIN_LINK = not bool(os.environ.get("META_ADS_DISABLE_LOGIN_LINK", ""))
38
+
35
39
 
36
- @mcp_server.tool()
37
40
  async def get_login_link(access_token: str = None) -> str:
38
41
  """
39
42
  Get a clickable login link for Meta Ads authentication.
@@ -195,4 +198,8 @@ async def get_login_link(access_token: str = None) -> str:
195
198
  # Wait a moment to ensure the server is fully started
196
199
  await asyncio.sleep(1)
197
200
 
198
- return json.dumps(response, indent=2)
201
+ return json.dumps(response, indent=2)
202
+
203
+ # Conditionally register as MCP tool only when enabled
204
+ if ENABLE_LOGIN_LINK:
205
+ get_login_link = mcp_server.tool()(get_login_link)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "meta-ads-mcp"
7
- version = "0.10.2"
7
+ version = "0.10.6"
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