meta-ads-mcp 0.7.8__tar.gz → 0.7.10__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.7.8 → meta_ads_mcp-0.7.10}/PKG-INFO +1 -1
- meta_ads_mcp-0.7.10/RELEASE.md +166 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/meta_ads_mcp/__init__.py +1 -1
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/ads.py +217 -20
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/utils.py +18 -6
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/pyproject.toml +1 -1
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/tests/test_get_ad_creatives_fix.py +3 -3
- meta_ads_mcp-0.7.10/tests/test_get_ad_image_quality_improvements.py +391 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/tests/test_get_ad_image_regression.py +74 -2
- meta_ads_mcp-0.7.10/tests/test_page_discovery.py +377 -0
- meta_ads_mcp-0.7.10/tests/test_page_discovery_integration.py +245 -0
- meta_ads_mcp-0.7.8/RELEASE.md +0 -97
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/.github/workflows/publish.yml +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/.github/workflows/test.yml +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/.gitignore +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/CUSTOM_META_APP.md +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/Dockerfile +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/LICENSE +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/LOCAL_INSTALLATION.md +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/META_API_NOTES.md +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/README.md +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/STREAMABLE_HTTP_SETUP.md +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/examples/README.md +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/examples/example_http_client.py +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/future_improvements.md +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/images/meta-ads-example.png +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/meta_ads_auth.sh +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/meta_ads_mcp/__main__.py +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/__init__.py +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/accounts.py +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/ads_library.py +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/adsets.py +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/api.py +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/auth.py +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/authentication.py +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/budget_schedules.py +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/callback_server.py +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/campaigns.py +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/duplication.py +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/http_auth_integration.py +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/insights.py +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/openai_deep_research.py +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/pipeboard_auth.py +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/reports.py +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/resources.py +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/server.py +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/targeting.py +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/requirements.txt +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/setup.py +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/smithery.yaml +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/tests/README.md +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/tests/README_REGRESSION_TESTS.md +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/tests/__init__.py +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/tests/conftest.py +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/tests/test_account_search.py +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/tests/test_budget_update.py +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/tests/test_budget_update_e2e.py +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/tests/test_dsa_beneficiary.py +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/tests/test_dsa_integration.py +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/tests/test_duplication.py +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/tests/test_duplication_regression.py +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/tests/test_http_transport.py +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/tests/test_insights_actions_and_values.py +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/tests/test_integration_openai_mcp.py +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/tests/test_openai.py +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/tests/test_openai_mcp_deep_research.py +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/tests/test_targeting.py +0 -0
- {meta_ads_mcp-0.7.8 → meta_ads_mcp-0.7.10}/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.7.
|
|
3
|
+
Version: 0.7.10
|
|
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
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Release Process
|
|
2
|
+
|
|
3
|
+
This repository uses GitHub Actions to automatically publish releases to PyPI. Here's the optimized release process:
|
|
4
|
+
|
|
5
|
+
## 🚀 Quick Release (Recommended)
|
|
6
|
+
|
|
7
|
+
### Prerequisites
|
|
8
|
+
- ✅ **Trusted Publishing Configured**: Repository uses PyPI trusted publishing with OIDC tokens
|
|
9
|
+
- ✅ **GitHub CLI installed**: `gh` command available for streamlined releases
|
|
10
|
+
- ✅ **Clean working directory**: No uncommitted changes
|
|
11
|
+
|
|
12
|
+
### Optimal Release Process
|
|
13
|
+
|
|
14
|
+
1. **Update version in both files** (use consistent versioning):
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# Update pyproject.toml
|
|
18
|
+
sed -i '' 's/version = "0.7.7"/version = "0.7.8"/' pyproject.toml
|
|
19
|
+
|
|
20
|
+
# Update __init__.py
|
|
21
|
+
sed -i '' 's/__version__ = "0.7.7"/__version__ = "0.7.8"/' meta_ads_mcp/__init__.py
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Or manually edit:
|
|
25
|
+
- `pyproject.toml`: `version = "0.7.8"`
|
|
26
|
+
- `meta_ads_mcp/__init__.py`: `__version__ = "0.7.8"`
|
|
27
|
+
|
|
28
|
+
2. **Commit and push version changes**:
|
|
29
|
+
```bash
|
|
30
|
+
git add pyproject.toml meta_ads_mcp/__init__.py
|
|
31
|
+
git commit -m "Bump version to 0.7.8"
|
|
32
|
+
git push origin main
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
3. **Create GitHub release** (triggers automatic PyPI publishing):
|
|
36
|
+
```bash
|
|
37
|
+
# Use bash wrapper if gh has issues in Cursor
|
|
38
|
+
bash -c "gh release create 0.7.8 --title '0.7.8' --generate-notes"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
4. **Verify release** (optional):
|
|
42
|
+
```bash
|
|
43
|
+
# Check GitHub release
|
|
44
|
+
curl -s "https://api.github.com/repos/pipeboard-co/meta-ads-mcp/releases/latest" | grep -E '"tag_name"|"name"'
|
|
45
|
+
|
|
46
|
+
# Check PyPI availability (wait 2-3 minutes)
|
|
47
|
+
curl -s "https://pypi.org/pypi/meta-ads-mcp/json" | grep -E '"version"|"0.7.8"'
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## 📋 Detailed Release Process
|
|
51
|
+
|
|
52
|
+
### Version Management Best Practices
|
|
53
|
+
|
|
54
|
+
- **Semantic Versioning**: Follow `MAJOR.MINOR.PATCH` (e.g., 0.7.8)
|
|
55
|
+
- **Synchronized Files**: Always update BOTH version files
|
|
56
|
+
- **Commit Convention**: Use `"Bump version to X.Y.Z"` format
|
|
57
|
+
- **Release Tag**: GitHub release tag matches version (no "v" prefix)
|
|
58
|
+
|
|
59
|
+
### Pre-Release Checklist
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# 1. Ensure clean working directory
|
|
63
|
+
git status
|
|
64
|
+
|
|
65
|
+
# 2. Run tests locally (optional but recommended)
|
|
66
|
+
uv run python -m pytest tests/ -v
|
|
67
|
+
|
|
68
|
+
# 3. Check current version
|
|
69
|
+
grep -E "version =|__version__" pyproject.toml meta_ads_mcp/__init__.py
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Release Commands (One-liner)
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# Complete release in one sequence
|
|
76
|
+
VERSION="0.7.8" && \
|
|
77
|
+
sed -i '' "s/version = \"0.7.7\"/version = \"$VERSION\"/" pyproject.toml && \
|
|
78
|
+
sed -i '' "s/__version__ = \"0.7.7\"/__version__ = \"$VERSION\"/" meta_ads_mcp/__init__.py && \
|
|
79
|
+
git add pyproject.toml meta_ads_mcp/__init__.py && \
|
|
80
|
+
git commit -m "Bump version to $VERSION" && \
|
|
81
|
+
git push origin main && \
|
|
82
|
+
bash -c "gh release create $VERSION --title '$VERSION' --generate-notes"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## 🔄 Workflows
|
|
86
|
+
|
|
87
|
+
### `publish.yml` (Automatic)
|
|
88
|
+
- **Trigger**: GitHub release creation
|
|
89
|
+
- **Purpose**: Build and publish to PyPI
|
|
90
|
+
- **Security**: OIDC tokens (no API keys)
|
|
91
|
+
- **Status**: ✅ Fully automated
|
|
92
|
+
|
|
93
|
+
### `test.yml` (Validation)
|
|
94
|
+
- **Trigger**: Push to main/master
|
|
95
|
+
- **Purpose**: Package structure validation
|
|
96
|
+
- **Matrix**: Python 3.10, 3.11, 3.12
|
|
97
|
+
- **Note**: Build tests only, not pytest
|
|
98
|
+
|
|
99
|
+
## 🛠️ Troubleshooting
|
|
100
|
+
|
|
101
|
+
### Common Issues
|
|
102
|
+
|
|
103
|
+
1. **gh command issues in Cursor**:
|
|
104
|
+
```bash
|
|
105
|
+
# Use bash wrapper
|
|
106
|
+
bash -c "gh release create 0.7.8 --title '0.7.8' --generate-notes"
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
2. **Version mismatch**:
|
|
110
|
+
```bash
|
|
111
|
+
# Verify both files have same version
|
|
112
|
+
grep -E "version =|__version__" pyproject.toml meta_ads_mcp/__init__.py
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
3. **PyPI not updated**:
|
|
116
|
+
```bash
|
|
117
|
+
# Check if package is available (wait 2-3 minutes)
|
|
118
|
+
curl -s "https://pypi.org/pypi/meta-ads-mcp/json" | grep '"version"'
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Manual Deployment (Fallback)
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
# Install build tools
|
|
125
|
+
pip install build twine
|
|
126
|
+
|
|
127
|
+
# Build package
|
|
128
|
+
python -m build
|
|
129
|
+
|
|
130
|
+
# Upload to PyPI (requires API token)
|
|
131
|
+
python -m twine upload dist/*
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## 📊 Release Verification
|
|
135
|
+
|
|
136
|
+
### GitHub Release
|
|
137
|
+
- ✅ Release created with correct tag
|
|
138
|
+
- ✅ Auto-generated notes from commits
|
|
139
|
+
- ✅ Actions tab shows successful workflow
|
|
140
|
+
|
|
141
|
+
### PyPI Package
|
|
142
|
+
- ✅ Package available for installation
|
|
143
|
+
- ✅ Correct version displayed
|
|
144
|
+
- ✅ All dependencies listed
|
|
145
|
+
|
|
146
|
+
### Installation Test
|
|
147
|
+
```bash
|
|
148
|
+
# Test new version installation
|
|
149
|
+
pip install meta-ads-mcp==0.7.8
|
|
150
|
+
# or
|
|
151
|
+
uvx meta-ads-mcp@0.7.8
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## 🔒 Security Notes
|
|
155
|
+
|
|
156
|
+
- **Trusted Publishing**: Uses GitHub OIDC tokens (no API keys needed)
|
|
157
|
+
- **Isolated Builds**: All builds run in GitHub-hosted runners
|
|
158
|
+
- **Access Control**: Only maintainers can create releases
|
|
159
|
+
- **Audit Trail**: All releases tracked in GitHub Actions
|
|
160
|
+
|
|
161
|
+
## 📈 Release Metrics
|
|
162
|
+
|
|
163
|
+
Track successful releases:
|
|
164
|
+
- **GitHub Releases**: https://github.com/pipeboard-co/meta-ads-mcp/releases
|
|
165
|
+
- **PyPI Package**: https://pypi.org/project/meta-ads-mcp/
|
|
166
|
+
- **Actions History**: https://github.com/pipeboard-co/meta-ads-mcp/actions
|
|
@@ -171,7 +171,7 @@ async def get_ad_creatives(access_token: str = None, ad_id: str = None) -> str:
|
|
|
171
171
|
|
|
172
172
|
endpoint = f"{ad_id}/adcreatives"
|
|
173
173
|
params = {
|
|
174
|
-
"fields": "id,name,status,thumbnail_url,image_url,image_hash,object_story_spec"
|
|
174
|
+
"fields": "id,name,status,thumbnail_url,image_url,image_hash,object_story_spec,asset_feed_spec,image_urls_for_viewing"
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
data = await make_api_request(endpoint, access_token, params)
|
|
@@ -279,14 +279,26 @@ async def get_ad_image(access_token: str = None, ad_id: str = None) -> Image:
|
|
|
279
279
|
if "data" in creative_data and creative_data["data"]:
|
|
280
280
|
creative = creative_data["data"][0]
|
|
281
281
|
|
|
282
|
-
#
|
|
282
|
+
# Prioritize higher quality image URLs in this order:
|
|
283
|
+
# 1. image_urls_for_viewing (usually highest quality)
|
|
284
|
+
# 2. image_url (direct field)
|
|
285
|
+
# 3. object_story_spec.link_data.picture (usually full size)
|
|
286
|
+
# 4. thumbnail_url (last resort - often profile thumbnail)
|
|
287
|
+
|
|
283
288
|
if "image_urls_for_viewing" in creative and creative["image_urls_for_viewing"]:
|
|
284
289
|
image_url = creative["image_urls_for_viewing"][0]
|
|
285
290
|
print(f"Using image_urls_for_viewing: {image_url}")
|
|
286
|
-
|
|
291
|
+
elif "image_url" in creative and creative["image_url"]:
|
|
292
|
+
image_url = creative["image_url"]
|
|
293
|
+
print(f"Using image_url: {image_url}")
|
|
294
|
+
elif "object_story_spec" in creative and "link_data" in creative["object_story_spec"]:
|
|
295
|
+
link_data = creative["object_story_spec"]["link_data"]
|
|
296
|
+
if "picture" in link_data and link_data["picture"]:
|
|
297
|
+
image_url = link_data["picture"]
|
|
298
|
+
print(f"Using object_story_spec.link_data.picture: {image_url}")
|
|
287
299
|
elif "thumbnail_url" in creative and creative["thumbnail_url"]:
|
|
288
300
|
image_url = creative["thumbnail_url"]
|
|
289
|
-
print(f"Using thumbnail_url: {image_url}")
|
|
301
|
+
print(f"Using thumbnail_url (fallback): {image_url}")
|
|
290
302
|
|
|
291
303
|
if not image_url:
|
|
292
304
|
return "Error: No image URLs found in creative"
|
|
@@ -658,31 +670,31 @@ async def create_ad_creative(
|
|
|
658
670
|
if not account_id.startswith("act_"):
|
|
659
671
|
account_id = f"act_{account_id}"
|
|
660
672
|
|
|
661
|
-
# If no page ID is provided,
|
|
673
|
+
# Enhanced page discovery: If no page ID is provided, use robust discovery methods
|
|
662
674
|
if not page_id:
|
|
663
675
|
try:
|
|
664
|
-
#
|
|
665
|
-
|
|
666
|
-
pages_params = {
|
|
667
|
-
"fields": "id,name",
|
|
668
|
-
"limit": 1
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
pages_data = await make_api_request(pages_endpoint, access_token, pages_params)
|
|
676
|
+
# Use the comprehensive page discovery logic from get_account_pages
|
|
677
|
+
page_discovery_result = await _discover_pages_for_account(account_id, access_token)
|
|
672
678
|
|
|
673
|
-
if "
|
|
674
|
-
page_id =
|
|
675
|
-
|
|
679
|
+
if page_discovery_result.get("success"):
|
|
680
|
+
page_id = page_discovery_result["page_id"]
|
|
681
|
+
page_name = page_discovery_result.get("page_name", "Unknown")
|
|
682
|
+
print(f"Auto-discovered page ID: {page_id} ({page_name})")
|
|
676
683
|
else:
|
|
677
684
|
return json.dumps({
|
|
678
|
-
"error": "No page ID provided and no pages found for this account",
|
|
679
|
-
"
|
|
685
|
+
"error": "No page ID provided and no suitable pages found for this account",
|
|
686
|
+
"details": page_discovery_result.get("message", "Page discovery failed"),
|
|
687
|
+
"suggestions": [
|
|
688
|
+
"Use get_account_pages to see available pages",
|
|
689
|
+
"Use search_pages_by_name to find specific pages",
|
|
690
|
+
"Provide a page_id parameter manually"
|
|
691
|
+
]
|
|
680
692
|
}, indent=2)
|
|
681
693
|
except Exception as e:
|
|
682
694
|
return json.dumps({
|
|
683
|
-
"error": "Error
|
|
695
|
+
"error": "Error during page discovery",
|
|
684
696
|
"details": str(e),
|
|
685
|
-
"suggestion": "Please provide a page_id parameter"
|
|
697
|
+
"suggestion": "Please provide a page_id parameter or use get_account_pages to find available pages"
|
|
686
698
|
}, indent=2)
|
|
687
699
|
|
|
688
700
|
# Prepare the creative data
|
|
@@ -747,6 +759,191 @@ async def create_ad_creative(
|
|
|
747
759
|
}, indent=2)
|
|
748
760
|
|
|
749
761
|
|
|
762
|
+
async def _discover_pages_for_account(account_id: str, access_token: str) -> dict:
|
|
763
|
+
"""
|
|
764
|
+
Internal function to discover pages for an account using multiple approaches.
|
|
765
|
+
Returns the best available page ID for ad creation.
|
|
766
|
+
"""
|
|
767
|
+
try:
|
|
768
|
+
# Approach 1: Extract page IDs from tracking_specs in ads (most reliable)
|
|
769
|
+
endpoint = f"{account_id}/ads"
|
|
770
|
+
params = {
|
|
771
|
+
"fields": "id,name,adset_id,campaign_id,status,creative,created_time,updated_time,bid_amount,conversion_domain,tracking_specs",
|
|
772
|
+
"limit": 100
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
tracking_ads_data = await make_api_request(endpoint, access_token, params)
|
|
776
|
+
|
|
777
|
+
tracking_page_ids = set()
|
|
778
|
+
if "data" in tracking_ads_data:
|
|
779
|
+
for ad in tracking_ads_data.get("data", []):
|
|
780
|
+
tracking_specs = ad.get("tracking_specs", [])
|
|
781
|
+
if isinstance(tracking_specs, list):
|
|
782
|
+
for spec in tracking_specs:
|
|
783
|
+
if isinstance(spec, dict) and "page" in spec:
|
|
784
|
+
page_list = spec["page"]
|
|
785
|
+
if isinstance(page_list, list):
|
|
786
|
+
for page_id in page_list:
|
|
787
|
+
if isinstance(page_id, (str, int)) and str(page_id).isdigit():
|
|
788
|
+
tracking_page_ids.add(str(page_id))
|
|
789
|
+
|
|
790
|
+
if tracking_page_ids:
|
|
791
|
+
# Get details for the first page found
|
|
792
|
+
page_id = list(tracking_page_ids)[0]
|
|
793
|
+
page_endpoint = f"{page_id}"
|
|
794
|
+
page_params = {
|
|
795
|
+
"fields": "id,name,username,category,fan_count,link,verification_status,picture"
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
page_data = await make_api_request(page_endpoint, access_token, page_params)
|
|
799
|
+
if "id" in page_data:
|
|
800
|
+
return {
|
|
801
|
+
"success": True,
|
|
802
|
+
"page_id": page_id,
|
|
803
|
+
"page_name": page_data.get("name", "Unknown"),
|
|
804
|
+
"source": "tracking_specs",
|
|
805
|
+
"note": "Page ID extracted from existing ads - most reliable for ad creation"
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
# Approach 2: Try client_pages endpoint
|
|
809
|
+
endpoint = f"{account_id}/client_pages"
|
|
810
|
+
params = {
|
|
811
|
+
"fields": "id,name,username,category,fan_count,link,verification_status,picture"
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
client_pages_data = await make_api_request(endpoint, access_token, params)
|
|
815
|
+
|
|
816
|
+
if "data" in client_pages_data and client_pages_data["data"]:
|
|
817
|
+
page = client_pages_data["data"][0]
|
|
818
|
+
return {
|
|
819
|
+
"success": True,
|
|
820
|
+
"page_id": page["id"],
|
|
821
|
+
"page_name": page.get("name", "Unknown"),
|
|
822
|
+
"source": "client_pages"
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
# Approach 3: Try assigned_pages endpoint
|
|
826
|
+
pages_endpoint = f"{account_id}/assigned_pages"
|
|
827
|
+
pages_params = {
|
|
828
|
+
"fields": "id,name",
|
|
829
|
+
"limit": 1
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
pages_data = await make_api_request(pages_endpoint, access_token, pages_params)
|
|
833
|
+
|
|
834
|
+
if "data" in pages_data and pages_data["data"]:
|
|
835
|
+
page = pages_data["data"][0]
|
|
836
|
+
return {
|
|
837
|
+
"success": True,
|
|
838
|
+
"page_id": page["id"],
|
|
839
|
+
"page_name": page.get("name", "Unknown"),
|
|
840
|
+
"source": "assigned_pages"
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
# If all approaches failed
|
|
844
|
+
return {
|
|
845
|
+
"success": False,
|
|
846
|
+
"message": "No suitable pages found for this account",
|
|
847
|
+
"note": "Try using get_account_pages to see all available pages or provide page_id manually"
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
except Exception as e:
|
|
851
|
+
return {
|
|
852
|
+
"success": False,
|
|
853
|
+
"message": f"Error during page discovery: {str(e)}"
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
async def _search_pages_by_name_core(access_token: str, account_id: str, search_term: str = None) -> str:
|
|
858
|
+
"""
|
|
859
|
+
Core logic for searching pages by name.
|
|
860
|
+
|
|
861
|
+
Args:
|
|
862
|
+
access_token: Meta API access token
|
|
863
|
+
account_id: Meta Ads account ID (format: act_XXXXXXXXX)
|
|
864
|
+
search_term: Search term to find pages by name (optional - returns all pages if not provided)
|
|
865
|
+
|
|
866
|
+
Returns:
|
|
867
|
+
JSON string with search results
|
|
868
|
+
"""
|
|
869
|
+
# Ensure account_id has the 'act_' prefix
|
|
870
|
+
if not account_id.startswith("act_"):
|
|
871
|
+
account_id = f"act_{account_id}"
|
|
872
|
+
|
|
873
|
+
try:
|
|
874
|
+
# Use the internal discovery function directly
|
|
875
|
+
page_discovery_result = await _discover_pages_for_account(account_id, access_token)
|
|
876
|
+
|
|
877
|
+
if not page_discovery_result.get("success"):
|
|
878
|
+
return json.dumps({
|
|
879
|
+
"data": [],
|
|
880
|
+
"message": "No pages found for this account",
|
|
881
|
+
"details": page_discovery_result.get("message", "Page discovery failed")
|
|
882
|
+
}, indent=2)
|
|
883
|
+
|
|
884
|
+
# Create a single page result
|
|
885
|
+
page_data = {
|
|
886
|
+
"id": page_discovery_result["page_id"],
|
|
887
|
+
"name": page_discovery_result.get("page_name", "Unknown"),
|
|
888
|
+
"source": page_discovery_result.get("source", "unknown")
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
all_pages_data = {"data": [page_data]}
|
|
892
|
+
|
|
893
|
+
# Filter pages by search term if provided
|
|
894
|
+
if search_term:
|
|
895
|
+
search_term_lower = search_term.lower()
|
|
896
|
+
filtered_pages = []
|
|
897
|
+
|
|
898
|
+
for page in all_pages_data["data"]:
|
|
899
|
+
page_name = page.get("name", "").lower()
|
|
900
|
+
if search_term_lower in page_name:
|
|
901
|
+
filtered_pages.append(page)
|
|
902
|
+
|
|
903
|
+
return json.dumps({
|
|
904
|
+
"data": filtered_pages,
|
|
905
|
+
"search_term": search_term,
|
|
906
|
+
"total_found": len(filtered_pages),
|
|
907
|
+
"total_available": len(all_pages_data["data"])
|
|
908
|
+
}, indent=2)
|
|
909
|
+
else:
|
|
910
|
+
# Return all pages if no search term provided
|
|
911
|
+
return json.dumps({
|
|
912
|
+
"data": all_pages_data["data"],
|
|
913
|
+
"total_available": len(all_pages_data["data"]),
|
|
914
|
+
"note": "Use search_term parameter to filter pages by name"
|
|
915
|
+
}, indent=2)
|
|
916
|
+
|
|
917
|
+
except Exception as e:
|
|
918
|
+
return json.dumps({
|
|
919
|
+
"error": "Failed to search pages by name",
|
|
920
|
+
"details": str(e)
|
|
921
|
+
}, indent=2)
|
|
922
|
+
|
|
923
|
+
|
|
924
|
+
@mcp_server.tool()
|
|
925
|
+
@meta_api_tool
|
|
926
|
+
async def search_pages_by_name(access_token: str = None, account_id: str = None, search_term: str = None) -> str:
|
|
927
|
+
"""
|
|
928
|
+
Search for pages by name within an account.
|
|
929
|
+
|
|
930
|
+
Args:
|
|
931
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
932
|
+
account_id: Meta Ads account ID (format: act_XXXXXXXXX)
|
|
933
|
+
search_term: Search term to find pages by name (optional - returns all pages if not provided)
|
|
934
|
+
|
|
935
|
+
Returns:
|
|
936
|
+
JSON response with matching pages
|
|
937
|
+
"""
|
|
938
|
+
# Check required parameters
|
|
939
|
+
if not account_id:
|
|
940
|
+
return json.dumps({"error": "No account ID provided"}, indent=2)
|
|
941
|
+
|
|
942
|
+
# Call the core function
|
|
943
|
+
result = await _search_pages_by_name_core(access_token, account_id, search_term)
|
|
944
|
+
return result
|
|
945
|
+
|
|
946
|
+
|
|
750
947
|
@mcp_server.tool()
|
|
751
948
|
@meta_api_tool
|
|
752
949
|
async def get_account_pages(access_token: str = None, account_id: str = None) -> str:
|
|
@@ -78,23 +78,31 @@ ad_creative_images = {}
|
|
|
78
78
|
def extract_creative_image_urls(creative: Dict[str, Any]) -> List[str]:
|
|
79
79
|
"""
|
|
80
80
|
Extract image URLs from a creative object for direct viewing.
|
|
81
|
+
Prioritizes higher quality images over thumbnails.
|
|
81
82
|
|
|
82
83
|
Args:
|
|
83
84
|
creative: Meta Ads creative object
|
|
84
85
|
|
|
85
86
|
Returns:
|
|
86
|
-
List of image URLs found in the creative
|
|
87
|
+
List of image URLs found in the creative, prioritized by quality
|
|
87
88
|
"""
|
|
88
89
|
image_urls = []
|
|
89
90
|
|
|
91
|
+
# Prioritize higher quality image URLs in this order:
|
|
92
|
+
# 1. image_urls_for_viewing (usually highest quality)
|
|
93
|
+
# 2. image_url (direct field)
|
|
94
|
+
# 3. object_story_spec.link_data.picture (usually full size)
|
|
95
|
+
# 4. asset_feed_spec images (multiple high-quality images)
|
|
96
|
+
# 5. thumbnail_url (last resort - often profile thumbnail)
|
|
97
|
+
|
|
98
|
+
# Check for image_urls_for_viewing (highest priority)
|
|
99
|
+
if "image_urls_for_viewing" in creative and creative["image_urls_for_viewing"]:
|
|
100
|
+
image_urls.extend(creative["image_urls_for_viewing"])
|
|
101
|
+
|
|
90
102
|
# Check for direct image_url field
|
|
91
103
|
if "image_url" in creative and creative["image_url"]:
|
|
92
104
|
image_urls.append(creative["image_url"])
|
|
93
105
|
|
|
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
106
|
# Check object_story_spec for image URLs
|
|
99
107
|
if "object_story_spec" in creative:
|
|
100
108
|
story_spec = creative["object_story_spec"]
|
|
@@ -103,7 +111,7 @@ def extract_creative_image_urls(creative: Dict[str, Any]) -> List[str]:
|
|
|
103
111
|
if "link_data" in story_spec:
|
|
104
112
|
link_data = story_spec["link_data"]
|
|
105
113
|
|
|
106
|
-
# Check for picture field
|
|
114
|
+
# Check for picture field (usually full size)
|
|
107
115
|
if "picture" in link_data and link_data["picture"]:
|
|
108
116
|
image_urls.append(link_data["picture"])
|
|
109
117
|
|
|
@@ -121,6 +129,10 @@ def extract_creative_image_urls(creative: Dict[str, Any]) -> List[str]:
|
|
|
121
129
|
if "url" in image and image["url"]:
|
|
122
130
|
image_urls.append(image["url"])
|
|
123
131
|
|
|
132
|
+
# Check for thumbnail_url field (lowest priority)
|
|
133
|
+
if "thumbnail_url" in creative and creative["thumbnail_url"]:
|
|
134
|
+
image_urls.append(creative["thumbnail_url"])
|
|
135
|
+
|
|
124
136
|
# Remove duplicates while preserving order
|
|
125
137
|
seen = set()
|
|
126
138
|
unique_urls = []
|
|
@@ -90,11 +90,11 @@ class TestGetAdCreativesBugFix:
|
|
|
90
90
|
|
|
91
91
|
urls = extract_creative_image_urls(test_creative)
|
|
92
92
|
|
|
93
|
-
# Should extract URLs in order: image_url,
|
|
93
|
+
# Should extract URLs in order: image_url, picture, thumbnail_url (new priority order)
|
|
94
94
|
expected_urls = [
|
|
95
95
|
"https://example.com/image.jpg",
|
|
96
|
-
"https://example.com/
|
|
97
|
-
"https://example.com/
|
|
96
|
+
"https://example.com/picture.jpg",
|
|
97
|
+
"https://example.com/thumb.jpg"
|
|
98
98
|
]
|
|
99
99
|
|
|
100
100
|
assert urls == expected_urls
|