meta-ads-mcp 0.4.6__tar.gz → 0.4.7__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 (51) hide show
  1. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/PKG-INFO +1 -1
  2. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/meta_ads_mcp/__init__.py +1 -1
  3. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/meta_ads_mcp/core/ads.py +2 -2
  4. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/meta_ads_mcp/core/utils.py +59 -1
  5. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/pyproject.toml +1 -1
  6. meta_ads_mcp-0.4.7/tests/test_get_ad_creatives_fix.py +154 -0
  7. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/.github/workflows/publish.yml +0 -0
  8. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/.github/workflows/test.yml +0 -0
  9. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/.gitignore +0 -0
  10. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/CUSTOM_META_APP.md +0 -0
  11. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/Dockerfile +0 -0
  12. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/LICENSE +0 -0
  13. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/LOCAL_INSTALLATION.md +0 -0
  14. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/META_API_NOTES.md +0 -0
  15. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/README.md +0 -0
  16. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/RELEASE.md +0 -0
  17. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/STREAMABLE_HTTP_SETUP.md +0 -0
  18. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/examples/README.md +0 -0
  19. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/examples/example_http_client.py +0 -0
  20. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/future_improvements.md +0 -0
  21. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/images/meta-ads-example.png +0 -0
  22. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/meta_ads_auth.sh +0 -0
  23. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/meta_ads_mcp/__main__.py +0 -0
  24. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/meta_ads_mcp/core/__init__.py +0 -0
  25. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/meta_ads_mcp/core/accounts.py +0 -0
  26. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/meta_ads_mcp/core/ads_library.py +0 -0
  27. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/meta_ads_mcp/core/adsets.py +0 -0
  28. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/meta_ads_mcp/core/api.py +0 -0
  29. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/meta_ads_mcp/core/auth.py +0 -0
  30. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/meta_ads_mcp/core/authentication.py +0 -0
  31. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/meta_ads_mcp/core/budget_schedules.py +0 -0
  32. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/meta_ads_mcp/core/callback_server.py +0 -0
  33. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/meta_ads_mcp/core/campaigns.py +0 -0
  34. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/meta_ads_mcp/core/duplication.py +0 -0
  35. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/meta_ads_mcp/core/http_auth_integration.py +0 -0
  36. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/meta_ads_mcp/core/insights.py +0 -0
  37. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/meta_ads_mcp/core/pipeboard_auth.py +0 -0
  38. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/meta_ads_mcp/core/reports.py +0 -0
  39. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/meta_ads_mcp/core/resources.py +0 -0
  40. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/meta_ads_mcp/core/server.py +0 -0
  41. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/requirements.txt +0 -0
  42. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/setup.py +0 -0
  43. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/smithery.yaml +0 -0
  44. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/tests/README.md +0 -0
  45. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/tests/README_REGRESSION_TESTS.md +0 -0
  46. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/tests/__init__.py +0 -0
  47. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/tests/conftest.py +0 -0
  48. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/tests/test_duplication.py +0 -0
  49. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/tests/test_duplication_regression.py +0 -0
  50. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/tests/test_http_transport.py +0 -0
  51. {meta_ads_mcp-0.4.6 → meta_ads_mcp-0.4.7}/tests/test_openai.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meta-ads-mcp
3
- Version: 0.4.6
3
+ Version: 0.4.7
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
@@ -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.4.6"
10
+ __version__ = "0.4.7"
11
11
 
12
12
  __all__ = [
13
13
  'get_ad_accounts',
@@ -10,7 +10,7 @@ import time
10
10
 
11
11
  from .api import meta_api_tool, make_api_request
12
12
  from .accounts import get_ad_accounts
13
- from .utils import download_image, try_multiple_download_methods, ad_creative_images
13
+ from .utils import download_image, try_multiple_download_methods, ad_creative_images, extract_creative_image_urls
14
14
  from .server import mcp_server
15
15
 
16
16
 
@@ -179,7 +179,7 @@ async def get_ad_creatives(access_token: str = None, ad_id: str = None) -> str:
179
179
  # Add image URLs for direct viewing if available
180
180
  if 'data' in data:
181
181
  for creative in data['data']:
182
- creative['image_urls_for_viewing'] = ad_creative_images(creative)
182
+ creative['image_urls_for_viewing'] = extract_creative_image_urls(creative)
183
183
 
184
184
  return json.dumps(data, indent=2)
185
185
 
@@ -1,6 +1,6 @@
1
1
  """Utility functions for Meta Ads API."""
2
2
 
3
- from typing import Optional, Dict, Any
3
+ from typing import Optional, Dict, Any, List
4
4
  import httpx
5
5
  import io
6
6
  from PIL import Image as PILImage
@@ -74,6 +74,64 @@ logger = setup_logging()
74
74
  # Global store for ad creative images
75
75
  ad_creative_images = {}
76
76
 
77
+
78
+ def extract_creative_image_urls(creative: Dict[str, Any]) -> List[str]:
79
+ """
80
+ Extract image URLs from a creative object for direct viewing.
81
+
82
+ Args:
83
+ creative: Meta Ads creative object
84
+
85
+ Returns:
86
+ List of image URLs found in the creative
87
+ """
88
+ image_urls = []
89
+
90
+ # Check for direct image_url field
91
+ if "image_url" in creative and creative["image_url"]:
92
+ image_urls.append(creative["image_url"])
93
+
94
+ # Check for thumbnail_url field
95
+ if "thumbnail_url" in creative and creative["thumbnail_url"]:
96
+ image_urls.append(creative["thumbnail_url"])
97
+
98
+ # Check object_story_spec for image URLs
99
+ if "object_story_spec" in creative:
100
+ story_spec = creative["object_story_spec"]
101
+
102
+ # Check link_data for image fields
103
+ if "link_data" in story_spec:
104
+ link_data = story_spec["link_data"]
105
+
106
+ # Check for picture field
107
+ if "picture" in link_data and link_data["picture"]:
108
+ image_urls.append(link_data["picture"])
109
+
110
+ # Check for image_url field in link_data
111
+ if "image_url" in link_data and link_data["image_url"]:
112
+ image_urls.append(link_data["image_url"])
113
+
114
+ # Check video_data for thumbnail (if present)
115
+ if "video_data" in story_spec and "image_url" in story_spec["video_data"]:
116
+ image_urls.append(story_spec["video_data"]["image_url"])
117
+
118
+ # Check asset_feed_spec for multiple images
119
+ if "asset_feed_spec" in creative and "images" in creative["asset_feed_spec"]:
120
+ for image in creative["asset_feed_spec"]["images"]:
121
+ if "url" in image and image["url"]:
122
+ image_urls.append(image["url"])
123
+
124
+ # Remove duplicates while preserving order
125
+ seen = set()
126
+ unique_urls = []
127
+ for url in image_urls:
128
+ if url not in seen:
129
+ seen.add(url)
130
+ unique_urls.append(url)
131
+
132
+ return unique_urls
133
+
134
+
77
135
  async def download_image(url: str) -> Optional[bytes]:
78
136
  """
79
137
  Download an image from a URL.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "meta-ads-mcp"
7
- version = "0.4.6"
7
+ version = "0.4.7"
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"
@@ -0,0 +1,154 @@
1
+ """Regression tests for get_ad_creatives function bug fix.
2
+
3
+ Tests for issue where get_ad_creatives would throw:
4
+ 'TypeError: 'dict' object is not callable'
5
+
6
+ This was caused by trying to call ad_creative_images(creative) where
7
+ ad_creative_images is a dictionary, not a function.
8
+
9
+ The fix was to create extract_creative_image_urls() function and use that instead.
10
+ """
11
+
12
+ import pytest
13
+ import json
14
+ from unittest.mock import AsyncMock, patch
15
+ from meta_ads_mcp.core.ads import get_ad_creatives
16
+ from meta_ads_mcp.core.utils import ad_creative_images
17
+
18
+
19
+ @pytest.mark.asyncio
20
+ class TestGetAdCreativesBugFix:
21
+ """Regression test cases for the get_ad_creatives function bug fix."""
22
+
23
+ async def test_get_ad_creatives_regression_fix(self):
24
+ """Regression test: ensure get_ad_creatives works correctly and doesn't throw 'dict' object is not callable."""
25
+
26
+ # Mock the make_api_request to return sample creative data
27
+ sample_creative_data = {
28
+ "data": [
29
+ {
30
+ "id": "123456789",
31
+ "name": "Test Creative",
32
+ "status": "ACTIVE",
33
+ "thumbnail_url": "https://example.com/thumb.jpg",
34
+ "image_url": "https://example.com/image.jpg",
35
+ "image_hash": "abc123",
36
+ "object_story_spec": {
37
+ "page_id": "987654321",
38
+ "link_data": {
39
+ "image_hash": "abc123",
40
+ "link": "https://example.com",
41
+ "name": "Test Ad"
42
+ }
43
+ }
44
+ }
45
+ ]
46
+ }
47
+
48
+ with patch('meta_ads_mcp.core.ads.make_api_request', new_callable=AsyncMock) as mock_api:
49
+ mock_api.return_value = sample_creative_data
50
+
51
+ # This should NOT raise a TypeError anymore
52
+ # Previously this would fail with: TypeError: 'dict' object is not callable
53
+ result = await get_ad_creatives(access_token="test_token", ad_id="120228922933270272")
54
+
55
+ # Parse the result
56
+ result_data = json.loads(result)
57
+
58
+ # Verify the structure is correct
59
+ assert "data" in result_data
60
+ assert len(result_data["data"]) == 1
61
+
62
+ creative = result_data["data"][0]
63
+ assert creative["id"] == "123456789"
64
+ assert creative["name"] == "Test Creative"
65
+ assert "image_urls_for_viewing" in creative
66
+ assert isinstance(creative["image_urls_for_viewing"], list)
67
+
68
+ async def test_extract_creative_image_urls_function(self):
69
+ """Test the extract_creative_image_urls function works correctly."""
70
+ from meta_ads_mcp.core.utils import extract_creative_image_urls
71
+
72
+ # Test creative with various image URL fields
73
+ test_creative = {
74
+ "id": "123456789",
75
+ "name": "Test Creative",
76
+ "status": "ACTIVE",
77
+ "thumbnail_url": "https://example.com/thumb.jpg",
78
+ "image_url": "https://example.com/image.jpg",
79
+ "image_hash": "abc123",
80
+ "object_story_spec": {
81
+ "page_id": "987654321",
82
+ "link_data": {
83
+ "image_hash": "abc123",
84
+ "link": "https://example.com",
85
+ "name": "Test Ad",
86
+ "picture": "https://example.com/picture.jpg"
87
+ }
88
+ }
89
+ }
90
+
91
+ urls = extract_creative_image_urls(test_creative)
92
+
93
+ # Should extract URLs in order: image_url, thumbnail_url, picture
94
+ expected_urls = [
95
+ "https://example.com/image.jpg",
96
+ "https://example.com/thumb.jpg",
97
+ "https://example.com/picture.jpg"
98
+ ]
99
+
100
+ assert urls == expected_urls
101
+
102
+ # Test with empty creative
103
+ empty_urls = extract_creative_image_urls({})
104
+ assert empty_urls == []
105
+
106
+ # Test with duplicates (should remove them)
107
+ duplicate_creative = {
108
+ "image_url": "https://example.com/same.jpg",
109
+ "thumbnail_url": "https://example.com/same.jpg", # Same URL
110
+ }
111
+ unique_urls = extract_creative_image_urls(duplicate_creative)
112
+ assert unique_urls == ["https://example.com/same.jpg"]
113
+
114
+ async def test_get_ad_creatives_no_ad_id(self):
115
+ """Test get_ad_creatives with no ad_id provided."""
116
+
117
+ result = await get_ad_creatives(access_token="test_token", ad_id=None)
118
+ result_data = json.loads(result)
119
+
120
+ # The @meta_api_tool decorator wraps the result in a data field
121
+ assert "data" in result_data
122
+ error_data = json.loads(result_data["data"])
123
+ assert "error" in error_data
124
+ assert error_data["error"] == "No ad ID provided"
125
+
126
+ async def test_get_ad_creatives_empty_data(self):
127
+ """Test get_ad_creatives when API returns empty data."""
128
+
129
+ empty_data = {"data": []}
130
+
131
+ with patch('meta_ads_mcp.core.ads.make_api_request', new_callable=AsyncMock) as mock_api:
132
+ mock_api.return_value = empty_data
133
+
134
+ result = await get_ad_creatives(access_token="test_token", ad_id="120228922933270272")
135
+ result_data = json.loads(result)
136
+
137
+ assert "data" in result_data
138
+ assert len(result_data["data"]) == 0
139
+
140
+
141
+ def test_ad_creative_images_is_dict():
142
+ """Test that ad_creative_images is a dictionary, not a function.
143
+
144
+ This confirms the original issue: ad_creative_images is a dict for storing image data,
145
+ but was being called as a function ad_creative_images(creative), which would fail.
146
+ This test ensures we don't accidentally change ad_creative_images to a function
147
+ and break its intended purpose as a storage dictionary.
148
+ """
149
+ assert isinstance(ad_creative_images, dict)
150
+
151
+ # This would raise TypeError: 'dict' object is not callable
152
+ # This is the original bug - trying to call a dict as a function
153
+ with pytest.raises(TypeError, match="'dict' object is not callable"):
154
+ ad_creative_images({"test": "data"})
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes