meta-ads-mcp 0.2.4__tar.gz → 0.2.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.
- {meta_ads_mcp-0.2.4 → meta_ads_mcp-0.2.6}/PKG-INFO +45 -14
- {meta_ads_mcp-0.2.4 → meta_ads_mcp-0.2.6}/README.md +44 -13
- meta_ads_mcp-0.2.6/future_improvements.md +11 -0
- meta_ads_mcp-0.2.6/images/meta-ads-example.png +0 -0
- {meta_ads_mcp-0.2.4 → meta_ads_mcp-0.2.6}/meta_ads_mcp/__init__.py +3 -1
- {meta_ads_mcp-0.2.4 → meta_ads_mcp-0.2.6}/meta_ads_mcp/core/ads.py +61 -1
- {meta_ads_mcp-0.2.4 → meta_ads_mcp-0.2.6}/meta_ads_mcp/core/adsets.py +33 -3
- {meta_ads_mcp-0.2.4 → meta_ads_mcp-0.2.6}/meta_ads_mcp/core/api.py +68 -24
- meta_ads_mcp-0.2.6/meta_ads_mcp/core/auth.py +446 -0
- {meta_ads_mcp-0.2.4 → meta_ads_mcp-0.2.6}/meta_ads_mcp/core/authentication.py +10 -1
- meta_ads_mcp-0.2.6/meta_ads_mcp/core/callback_server.py +958 -0
- {meta_ads_mcp-0.2.4 → meta_ads_mcp-0.2.6}/meta_ads_mcp/core/utils.py +10 -0
- {meta_ads_mcp-0.2.4 → meta_ads_mcp-0.2.6}/pyproject.toml +1 -1
- meta_ads_mcp-0.2.4/future_improvements.md +0 -63
- meta_ads_mcp-0.2.4/meta_ads_mcp/core/auth.py +0 -863
- {meta_ads_mcp-0.2.4 → meta_ads_mcp-0.2.6}/.gitignore +0 -0
- {meta_ads_mcp-0.2.4 → meta_ads_mcp-0.2.6}/META_API_NOTES.md +0 -0
- {meta_ads_mcp-0.2.4 → meta_ads_mcp-0.2.6}/meta-ads-mcp +0 -0
- {meta_ads_mcp-0.2.4 → meta_ads_mcp-0.2.6}/meta_ads_mcp/__main__.py +0 -0
- {meta_ads_mcp-0.2.4 → meta_ads_mcp-0.2.6}/meta_ads_mcp/api.py +0 -0
- {meta_ads_mcp-0.2.4 → meta_ads_mcp-0.2.6}/meta_ads_mcp/core/__init__.py +0 -0
- {meta_ads_mcp-0.2.4 → meta_ads_mcp-0.2.6}/meta_ads_mcp/core/accounts.py +0 -0
- {meta_ads_mcp-0.2.4 → meta_ads_mcp-0.2.6}/meta_ads_mcp/core/campaigns.py +0 -0
- {meta_ads_mcp-0.2.4 → meta_ads_mcp-0.2.6}/meta_ads_mcp/core/insights.py +0 -0
- {meta_ads_mcp-0.2.4 → meta_ads_mcp-0.2.6}/meta_ads_mcp/core/resources.py +0 -0
- {meta_ads_mcp-0.2.4 → meta_ads_mcp-0.2.6}/meta_ads_mcp/core/server.py +0 -0
- {meta_ads_mcp-0.2.4 → meta_ads_mcp-0.2.6}/requirements.txt +0 -0
- {meta_ads_mcp-0.2.4 → meta_ads_mcp-0.2.6}/setup.py +0 -0
- {meta_ads_mcp-0.2.4 → meta_ads_mcp-0.2.6}/test_meta_ads_auth.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meta-ads-mcp
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
4
4
|
Summary: Model Calling Protocol (MCP) plugin for interacting with Meta Ads API
|
|
5
5
|
Project-URL: Homepage, https://github.com/nictuku/meta-ads-mcp
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/nictuku/meta-ads-mcp/issues
|
|
@@ -21,16 +21,26 @@ Description-Content-Type: text/markdown
|
|
|
21
21
|
|
|
22
22
|
# Meta Ads MCP
|
|
23
23
|
|
|
24
|
-
A [Model
|
|
24
|
+
A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server for interacting with Meta Ads API. This tool enables AI models to access, analyze, and manage Meta advertising campaigns through a standardized interface, allowing LLMs to retrieve performance data, visualize ad creatives, and provide strategic insights for Facebook, Instagram, and other Meta platforms.
|
|
25
|
+
|
|
26
|
+
> **DISCLAIMER:** This is an unofficial third-party tool and is not associated with, endorsed by, or affiliated with Meta in any way. This project is maintained independently and uses Meta's public APIs according to their terms of service. Meta, Facebook, Instagram, and other Meta brand names are trademarks of their respective owners.
|
|
27
|
+
|
|
28
|
+
Screenhot: using an LLM to understand your ad performance.
|
|
29
|
+
|
|
30
|
+

|
|
25
31
|
|
|
26
32
|
## Features
|
|
27
33
|
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
+
- **AI-Powered Campaign Analysis**: Let your favorite LLM analyze your campaigns and provide actionable insights on performance
|
|
35
|
+
- **Strategic Recommendations**: Receive data-backed suggestions for optimizing ad spend, targeting, and creative content
|
|
36
|
+
- **Automated Monitoring**: Ask any MCP-compatible LLM to track performance metrics and alert you about significant changes
|
|
37
|
+
- **Budget Optimization**: Get recommendations for reallocating budget to better-performing ad sets
|
|
38
|
+
- **Creative Improvement**: Receive feedback on ad copy, imagery, and calls-to-action
|
|
39
|
+
- **Campaign Management**: Request changes to campaigns, ad sets, and ads (all changes require explicit confirmation)
|
|
40
|
+
- **Cross-Platform Integration**: Works with Facebook, Instagram, and all Meta ad platforms
|
|
41
|
+
- **Universal LLM Support**: Compatible with any MCP client including Claude Desktop, Cursor, Cherry Studio, and more
|
|
42
|
+
- **Simple Authentication**: Easy setup with secure OAuth authentication
|
|
43
|
+
- **Cross-Platform Support**: Works on Windows, macOS, and Linux
|
|
34
44
|
|
|
35
45
|
## Installation
|
|
36
46
|
|
|
@@ -39,7 +49,7 @@ A [Model Calling Protocol (MCP)](https://github.com/anthropics/anthropic-tools)
|
|
|
39
49
|
When using uv no specific installation is needed. We can use uvx to directly run meta-ads-mcp:
|
|
40
50
|
|
|
41
51
|
```bash
|
|
42
|
-
uvx meta-ads-mcp
|
|
52
|
+
uvx meta-ads-mcp --app-id YOUR_META_ADS_APP_ID
|
|
43
53
|
```
|
|
44
54
|
|
|
45
55
|
If you want to install the package:
|
|
@@ -79,7 +89,7 @@ Add this to your `claude_desktop_config.json` to integrate with Claude in Cursor
|
|
|
79
89
|
"mcpServers": {
|
|
80
90
|
"meta-ads": {
|
|
81
91
|
"command": "uvx",
|
|
82
|
-
"args": ["meta-ads-mcp"]
|
|
92
|
+
"args": ["meta-ads-mcp", "--app-id", "YOUR_META_ADS_APP_ID"]
|
|
83
93
|
}
|
|
84
94
|
}
|
|
85
95
|
```
|
|
@@ -177,7 +187,28 @@ Add this to your `claude_desktop_config.json` to integrate with Claude in Cursor
|
|
|
177
187
|
- `ad_id`: Meta Ads ad ID
|
|
178
188
|
- Returns: The ad image ready for direct visual analysis
|
|
179
189
|
|
|
180
|
-
12. `
|
|
190
|
+
12. `mcp_meta_ads_update_ad`
|
|
191
|
+
- Update an ad with new settings
|
|
192
|
+
- Inputs:
|
|
193
|
+
- `ad_id`: Meta Ads ad ID
|
|
194
|
+
- `status`: Update ad status (ACTIVE, PAUSED, etc.)
|
|
195
|
+
- `bid_amount`: Bid amount in account currency (in cents for USD)
|
|
196
|
+
- `access_token` (optional): Meta API access token (will use cached token if not provided)
|
|
197
|
+
- Returns: Confirmation with updated ad details and a confirmation link
|
|
198
|
+
|
|
199
|
+
13. `mcp_meta_ads_update_adset`
|
|
200
|
+
- Update an ad set with new settings including frequency caps
|
|
201
|
+
- Inputs:
|
|
202
|
+
- `adset_id`: Meta Ads ad set ID
|
|
203
|
+
- `frequency_control_specs`: List of frequency control specifications
|
|
204
|
+
- `bid_strategy`: Bid strategy (e.g., 'LOWEST_COST_WITH_BID_CAP')
|
|
205
|
+
- `bid_amount`: Bid amount in account currency (in cents for USD)
|
|
206
|
+
- `status`: Update ad set status (ACTIVE, PAUSED, etc.)
|
|
207
|
+
- `targeting`: Targeting specifications including targeting_automation
|
|
208
|
+
- `access_token` (optional): Meta API access token (will use cached token if not provided)
|
|
209
|
+
- Returns: Confirmation with updated ad set details and a confirmation link
|
|
210
|
+
|
|
211
|
+
14. `mcp_meta_ads_get_insights`
|
|
181
212
|
- Get performance insights for a campaign, ad set, ad or account
|
|
182
213
|
- Inputs:
|
|
183
214
|
- `access_token` (optional): Meta API access token (will use cached token if not provided)
|
|
@@ -187,7 +218,7 @@ Add this to your `claude_desktop_config.json` to integrate with Claude in Cursor
|
|
|
187
218
|
- `level`: Level of aggregation (ad, adset, campaign, account)
|
|
188
219
|
- Returns: Performance metrics for the specified object
|
|
189
220
|
|
|
190
|
-
|
|
221
|
+
15. `mcp_meta_ads_debug_image_download`
|
|
191
222
|
- Debug image download issues and report detailed diagnostics
|
|
192
223
|
- Inputs:
|
|
193
224
|
- `access_token` (optional): Meta API access token (will use cached token if not provided)
|
|
@@ -195,7 +226,7 @@ Add this to your `claude_desktop_config.json` to integrate with Claude in Cursor
|
|
|
195
226
|
- `ad_id`: Meta Ads ad ID (optional, used if url is not provided)
|
|
196
227
|
- Returns: Diagnostic information about image download attempts
|
|
197
228
|
|
|
198
|
-
|
|
229
|
+
16. `mcp_meta_ads_get_login_link`
|
|
199
230
|
- Get a clickable login link for Meta Ads authentication
|
|
200
231
|
- Inputs:
|
|
201
232
|
- `access_token` (optional): Meta API access token (will use cached token if not provided)
|
|
@@ -350,4 +381,4 @@ You can check the current version of the package:
|
|
|
350
381
|
```python
|
|
351
382
|
import meta_ads_mcp
|
|
352
383
|
print(meta_ads_mcp.__version__)
|
|
353
|
-
```
|
|
384
|
+
```
|
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
# Meta Ads MCP
|
|
2
2
|
|
|
3
|
-
A [Model
|
|
3
|
+
A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server for interacting with Meta Ads API. This tool enables AI models to access, analyze, and manage Meta advertising campaigns through a standardized interface, allowing LLMs to retrieve performance data, visualize ad creatives, and provide strategic insights for Facebook, Instagram, and other Meta platforms.
|
|
4
|
+
|
|
5
|
+
> **DISCLAIMER:** This is an unofficial third-party tool and is not associated with, endorsed by, or affiliated with Meta in any way. This project is maintained independently and uses Meta's public APIs according to their terms of service. Meta, Facebook, Instagram, and other Meta brand names are trademarks of their respective owners.
|
|
6
|
+
|
|
7
|
+
Screenhot: using an LLM to understand your ad performance.
|
|
8
|
+
|
|
9
|
+

|
|
4
10
|
|
|
5
11
|
## Features
|
|
6
12
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
+
- **AI-Powered Campaign Analysis**: Let your favorite LLM analyze your campaigns and provide actionable insights on performance
|
|
14
|
+
- **Strategic Recommendations**: Receive data-backed suggestions for optimizing ad spend, targeting, and creative content
|
|
15
|
+
- **Automated Monitoring**: Ask any MCP-compatible LLM to track performance metrics and alert you about significant changes
|
|
16
|
+
- **Budget Optimization**: Get recommendations for reallocating budget to better-performing ad sets
|
|
17
|
+
- **Creative Improvement**: Receive feedback on ad copy, imagery, and calls-to-action
|
|
18
|
+
- **Campaign Management**: Request changes to campaigns, ad sets, and ads (all changes require explicit confirmation)
|
|
19
|
+
- **Cross-Platform Integration**: Works with Facebook, Instagram, and all Meta ad platforms
|
|
20
|
+
- **Universal LLM Support**: Compatible with any MCP client including Claude Desktop, Cursor, Cherry Studio, and more
|
|
21
|
+
- **Simple Authentication**: Easy setup with secure OAuth authentication
|
|
22
|
+
- **Cross-Platform Support**: Works on Windows, macOS, and Linux
|
|
13
23
|
|
|
14
24
|
## Installation
|
|
15
25
|
|
|
@@ -18,7 +28,7 @@ A [Model Calling Protocol (MCP)](https://github.com/anthropics/anthropic-tools)
|
|
|
18
28
|
When using uv no specific installation is needed. We can use uvx to directly run meta-ads-mcp:
|
|
19
29
|
|
|
20
30
|
```bash
|
|
21
|
-
uvx meta-ads-mcp
|
|
31
|
+
uvx meta-ads-mcp --app-id YOUR_META_ADS_APP_ID
|
|
22
32
|
```
|
|
23
33
|
|
|
24
34
|
If you want to install the package:
|
|
@@ -58,7 +68,7 @@ Add this to your `claude_desktop_config.json` to integrate with Claude in Cursor
|
|
|
58
68
|
"mcpServers": {
|
|
59
69
|
"meta-ads": {
|
|
60
70
|
"command": "uvx",
|
|
61
|
-
"args": ["meta-ads-mcp"]
|
|
71
|
+
"args": ["meta-ads-mcp", "--app-id", "YOUR_META_ADS_APP_ID"]
|
|
62
72
|
}
|
|
63
73
|
}
|
|
64
74
|
```
|
|
@@ -156,7 +166,28 @@ Add this to your `claude_desktop_config.json` to integrate with Claude in Cursor
|
|
|
156
166
|
- `ad_id`: Meta Ads ad ID
|
|
157
167
|
- Returns: The ad image ready for direct visual analysis
|
|
158
168
|
|
|
159
|
-
12. `
|
|
169
|
+
12. `mcp_meta_ads_update_ad`
|
|
170
|
+
- Update an ad with new settings
|
|
171
|
+
- Inputs:
|
|
172
|
+
- `ad_id`: Meta Ads ad ID
|
|
173
|
+
- `status`: Update ad status (ACTIVE, PAUSED, etc.)
|
|
174
|
+
- `bid_amount`: Bid amount in account currency (in cents for USD)
|
|
175
|
+
- `access_token` (optional): Meta API access token (will use cached token if not provided)
|
|
176
|
+
- Returns: Confirmation with updated ad details and a confirmation link
|
|
177
|
+
|
|
178
|
+
13. `mcp_meta_ads_update_adset`
|
|
179
|
+
- Update an ad set with new settings including frequency caps
|
|
180
|
+
- Inputs:
|
|
181
|
+
- `adset_id`: Meta Ads ad set ID
|
|
182
|
+
- `frequency_control_specs`: List of frequency control specifications
|
|
183
|
+
- `bid_strategy`: Bid strategy (e.g., 'LOWEST_COST_WITH_BID_CAP')
|
|
184
|
+
- `bid_amount`: Bid amount in account currency (in cents for USD)
|
|
185
|
+
- `status`: Update ad set status (ACTIVE, PAUSED, etc.)
|
|
186
|
+
- `targeting`: Targeting specifications including targeting_automation
|
|
187
|
+
- `access_token` (optional): Meta API access token (will use cached token if not provided)
|
|
188
|
+
- Returns: Confirmation with updated ad set details and a confirmation link
|
|
189
|
+
|
|
190
|
+
14. `mcp_meta_ads_get_insights`
|
|
160
191
|
- Get performance insights for a campaign, ad set, ad or account
|
|
161
192
|
- Inputs:
|
|
162
193
|
- `access_token` (optional): Meta API access token (will use cached token if not provided)
|
|
@@ -166,7 +197,7 @@ Add this to your `claude_desktop_config.json` to integrate with Claude in Cursor
|
|
|
166
197
|
- `level`: Level of aggregation (ad, adset, campaign, account)
|
|
167
198
|
- Returns: Performance metrics for the specified object
|
|
168
199
|
|
|
169
|
-
|
|
200
|
+
15. `mcp_meta_ads_debug_image_download`
|
|
170
201
|
- Debug image download issues and report detailed diagnostics
|
|
171
202
|
- Inputs:
|
|
172
203
|
- `access_token` (optional): Meta API access token (will use cached token if not provided)
|
|
@@ -174,7 +205,7 @@ Add this to your `claude_desktop_config.json` to integrate with Claude in Cursor
|
|
|
174
205
|
- `ad_id`: Meta Ads ad ID (optional, used if url is not provided)
|
|
175
206
|
- Returns: Diagnostic information about image download attempts
|
|
176
207
|
|
|
177
|
-
|
|
208
|
+
16. `mcp_meta_ads_get_login_link`
|
|
178
209
|
- Get a clickable login link for Meta Ads authentication
|
|
179
210
|
- Inputs:
|
|
180
211
|
- `access_token` (optional): Meta API access token (will use cached token if not provided)
|
|
@@ -329,4 +360,4 @@ You can check the current version of the package:
|
|
|
329
360
|
```python
|
|
330
361
|
import meta_ads_mcp
|
|
331
362
|
print(meta_ads_mcp.__version__)
|
|
332
|
-
```
|
|
363
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Future Improvements for Meta Ads MCP
|
|
2
|
+
|
|
3
|
+
## Note about Meta Ads development work
|
|
4
|
+
|
|
5
|
+
If you update the MCP server code, please note that *I* have to restart the MCP server. After the server code is changed, ask me to restart it and then proceed with your testing after I confirm it's restarted.
|
|
6
|
+
|
|
7
|
+
## Access token should remain internal only
|
|
8
|
+
|
|
9
|
+
Don't share it ever with the LLM, only update the auth cache.
|
|
10
|
+
|
|
11
|
+
Future improvements can be added to this file as needed.
|
|
Binary file
|
|
@@ -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.2.
|
|
10
|
+
__version__ = "0.2.6"
|
|
11
11
|
|
|
12
12
|
__all__ = [
|
|
13
13
|
'get_ad_accounts',
|
|
@@ -22,6 +22,7 @@ __all__ = [
|
|
|
22
22
|
'get_ad_details',
|
|
23
23
|
'get_ad_creatives',
|
|
24
24
|
'get_ad_image',
|
|
25
|
+
'update_ad',
|
|
25
26
|
'get_insights',
|
|
26
27
|
'debug_image_download',
|
|
27
28
|
'get_login_link',
|
|
@@ -43,6 +44,7 @@ from .core import (
|
|
|
43
44
|
get_ad_details,
|
|
44
45
|
get_ad_creatives,
|
|
45
46
|
get_ad_image,
|
|
47
|
+
update_ad,
|
|
46
48
|
get_insights,
|
|
47
49
|
debug_image_download,
|
|
48
50
|
get_login_link,
|
|
@@ -363,4 +363,64 @@ async def get_ad_image(access_token: str = None, ad_id: str = None) -> Image:
|
|
|
363
363
|
return Image(data=img_bytes, format="jpeg")
|
|
364
364
|
|
|
365
365
|
except Exception as e:
|
|
366
|
-
return f"Error processing image: {str(e)}"
|
|
366
|
+
return f"Error processing image: {str(e)}"
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
@mcp_server.tool()
|
|
370
|
+
@meta_api_tool
|
|
371
|
+
async def update_ad(ad_id: str, status: str = None, bid_amount: int = None, access_token: str = None) -> str:
|
|
372
|
+
"""
|
|
373
|
+
Update an ad with new settings.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
ad_id: Meta Ads ad ID
|
|
377
|
+
status: Update ad status (ACTIVE, PAUSED, etc.)
|
|
378
|
+
bid_amount: Bid amount in account currency (in cents for USD)
|
|
379
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
380
|
+
"""
|
|
381
|
+
if not ad_id:
|
|
382
|
+
return json.dumps({"error": "No ad ID provided"}, indent=2)
|
|
383
|
+
|
|
384
|
+
changes = {}
|
|
385
|
+
|
|
386
|
+
if status is not None:
|
|
387
|
+
changes['status'] = status
|
|
388
|
+
|
|
389
|
+
if bid_amount is not None:
|
|
390
|
+
changes['bid_amount'] = bid_amount
|
|
391
|
+
|
|
392
|
+
if not changes:
|
|
393
|
+
return json.dumps({"error": "No update parameters provided"}, indent=2)
|
|
394
|
+
|
|
395
|
+
# Get current ad details for comparison
|
|
396
|
+
current_details_json = await get_ad_details(ad_id=ad_id, access_token=access_token)
|
|
397
|
+
current_details = json.loads(current_details_json)
|
|
398
|
+
|
|
399
|
+
# Import the callback server components
|
|
400
|
+
from .callback_server import start_callback_server, update_confirmation
|
|
401
|
+
import urllib.parse
|
|
402
|
+
|
|
403
|
+
# Start the callback server if not already running
|
|
404
|
+
port = start_callback_server()
|
|
405
|
+
|
|
406
|
+
# Generate confirmation URL with properly encoded parameters
|
|
407
|
+
changes_json = json.dumps(changes)
|
|
408
|
+
encoded_changes = urllib.parse.quote(changes_json)
|
|
409
|
+
confirmation_url = f"http://localhost:{port}/confirm-update?ad_id={ad_id}&token={access_token}&changes={encoded_changes}"
|
|
410
|
+
|
|
411
|
+
# Reset the update confirmation
|
|
412
|
+
update_confirmation.clear()
|
|
413
|
+
update_confirmation.update({"approved": False})
|
|
414
|
+
|
|
415
|
+
# Return the confirmation link
|
|
416
|
+
response = {
|
|
417
|
+
"message": "Please confirm the ad update",
|
|
418
|
+
"confirmation_url": confirmation_url,
|
|
419
|
+
"markdown_link": f"[Click here to confirm ad update]({confirmation_url})",
|
|
420
|
+
"current_details": current_details,
|
|
421
|
+
"proposed_changes": changes,
|
|
422
|
+
"instructions_for_llm": "You must present this link as clickable Markdown to the user using the markdown_link format provided.",
|
|
423
|
+
"note": "After authenticating, the token will be automatically saved and your ad will be updated. Refresh the browser page if it doesn't load immediately."
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return json.dumps(response, indent=2)
|
|
@@ -6,7 +6,8 @@ from .api import meta_api_tool, make_api_request
|
|
|
6
6
|
from .accounts import get_ad_accounts
|
|
7
7
|
from .server import mcp_server
|
|
8
8
|
import asyncio
|
|
9
|
-
from .
|
|
9
|
+
from .callback_server import start_callback_server, update_confirmation
|
|
10
|
+
import urllib.parse
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
@mcp_server.tool()
|
|
@@ -77,7 +78,7 @@ async def get_adset_details(access_token: str = None, adset_id: str = None) -> s
|
|
|
77
78
|
@mcp_server.tool()
|
|
78
79
|
@meta_api_tool
|
|
79
80
|
async def update_adset(adset_id: str, frequency_control_specs: List[Dict[str, Any]] = None, bid_strategy: str = None,
|
|
80
|
-
bid_amount: int = None, status: str = None, access_token: str = None) -> str:
|
|
81
|
+
bid_amount: int = None, status: str = None, targeting: Dict[str, Any] = None, access_token: str = None) -> str:
|
|
81
82
|
"""
|
|
82
83
|
Update an ad set with new settings including frequency caps.
|
|
83
84
|
|
|
@@ -88,6 +89,8 @@ async def update_adset(adset_id: str, frequency_control_specs: List[Dict[str, An
|
|
|
88
89
|
bid_strategy: Bid strategy (e.g., 'LOWEST_COST_WITH_BID_CAP')
|
|
89
90
|
bid_amount: Bid amount in account currency (in cents for USD)
|
|
90
91
|
status: Update ad set status (ACTIVE, PAUSED, etc.)
|
|
92
|
+
targeting: Targeting specifications including targeting_automation
|
|
93
|
+
(e.g. {"targeting_automation":{"advantage_audience":1}})
|
|
91
94
|
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
92
95
|
"""
|
|
93
96
|
if not adset_id:
|
|
@@ -106,6 +109,32 @@ async def update_adset(adset_id: str, frequency_control_specs: List[Dict[str, An
|
|
|
106
109
|
|
|
107
110
|
if status is not None:
|
|
108
111
|
changes['status'] = status
|
|
112
|
+
|
|
113
|
+
if targeting is not None:
|
|
114
|
+
# Get current ad set details to preserve existing targeting settings
|
|
115
|
+
current_details_json = await get_adset_details(adset_id=adset_id, access_token=access_token)
|
|
116
|
+
current_details = json.loads(current_details_json)
|
|
117
|
+
|
|
118
|
+
# Check if the current ad set has targeting information
|
|
119
|
+
current_targeting = current_details.get('targeting', {})
|
|
120
|
+
|
|
121
|
+
if 'targeting_automation' in targeting:
|
|
122
|
+
# Only update targeting_automation while preserving other targeting settings
|
|
123
|
+
if current_targeting:
|
|
124
|
+
merged_targeting = current_targeting.copy()
|
|
125
|
+
merged_targeting['targeting_automation'] = targeting['targeting_automation']
|
|
126
|
+
changes['targeting'] = merged_targeting
|
|
127
|
+
else:
|
|
128
|
+
# If there's no existing targeting, we need to create a basic one
|
|
129
|
+
# Meta requires at least a geo_locations setting
|
|
130
|
+
basic_targeting = {
|
|
131
|
+
'targeting_automation': targeting['targeting_automation'],
|
|
132
|
+
'geo_locations': {'countries': ['US']} # Using US as default location
|
|
133
|
+
}
|
|
134
|
+
changes['targeting'] = basic_targeting
|
|
135
|
+
else:
|
|
136
|
+
# Full targeting replacement
|
|
137
|
+
changes['targeting'] = targeting
|
|
109
138
|
|
|
110
139
|
if not changes:
|
|
111
140
|
return json.dumps({"error": "No update parameters provided"}, indent=2)
|
|
@@ -119,7 +148,8 @@ async def update_adset(adset_id: str, frequency_control_specs: List[Dict[str, An
|
|
|
119
148
|
|
|
120
149
|
# Generate confirmation URL with properly encoded parameters
|
|
121
150
|
changes_json = json.dumps(changes)
|
|
122
|
-
|
|
151
|
+
encoded_changes = urllib.parse.quote(changes_json)
|
|
152
|
+
confirmation_url = f"http://localhost:{port}/confirm-update?adset_id={adset_id}&token={access_token}&changes={encoded_changes}"
|
|
123
153
|
|
|
124
154
|
# Reset the update confirmation
|
|
125
155
|
update_confirmation.clear()
|
|
@@ -59,9 +59,9 @@ async def make_api_request(
|
|
|
59
59
|
if not access_token:
|
|
60
60
|
logger.error("API request attempted with blank access token")
|
|
61
61
|
return {
|
|
62
|
-
"error":
|
|
63
|
-
|
|
64
|
-
"
|
|
62
|
+
"error": {
|
|
63
|
+
"message": "Authentication Required",
|
|
64
|
+
"details": "A valid access token is required to access the Meta API",
|
|
65
65
|
"action_required": "Please authenticate first"
|
|
66
66
|
}
|
|
67
67
|
}
|
|
@@ -89,7 +89,18 @@ async def make_api_request(
|
|
|
89
89
|
if method == "GET":
|
|
90
90
|
response = await client.get(url, params=request_params, headers=headers, timeout=30.0)
|
|
91
91
|
elif method == "POST":
|
|
92
|
-
|
|
92
|
+
# For Meta API, POST requests need data, not JSON
|
|
93
|
+
if 'targeting' in request_params and isinstance(request_params['targeting'], dict):
|
|
94
|
+
# Convert targeting dict to string for the API
|
|
95
|
+
request_params['targeting'] = json.dumps(request_params['targeting'])
|
|
96
|
+
|
|
97
|
+
# Convert lists and dicts to JSON strings
|
|
98
|
+
for key, value in request_params.items():
|
|
99
|
+
if isinstance(value, (list, dict)):
|
|
100
|
+
request_params[key] = json.dumps(value)
|
|
101
|
+
|
|
102
|
+
logger.debug(f"POST params (prepared): {masked_params}")
|
|
103
|
+
response = await client.post(url, data=request_params, headers=headers, timeout=30.0)
|
|
93
104
|
elif method == "DELETE":
|
|
94
105
|
response = await client.delete(url, params=request_params, headers=headers, timeout=30.0)
|
|
95
106
|
else:
|
|
@@ -97,7 +108,16 @@ async def make_api_request(
|
|
|
97
108
|
|
|
98
109
|
response.raise_for_status()
|
|
99
110
|
logger.debug(f"API Response status: {response.status_code}")
|
|
100
|
-
|
|
111
|
+
|
|
112
|
+
# Ensure the response is JSON and return it as a dictionary
|
|
113
|
+
try:
|
|
114
|
+
return response.json()
|
|
115
|
+
except json.JSONDecodeError:
|
|
116
|
+
# If not JSON, return text content in a structured format
|
|
117
|
+
return {
|
|
118
|
+
"text_response": response.text,
|
|
119
|
+
"status_code": response.status_code
|
|
120
|
+
}
|
|
101
121
|
|
|
102
122
|
except httpx.HTTPStatusError as e:
|
|
103
123
|
error_info = {}
|
|
@@ -123,8 +143,7 @@ async def make_api_request(
|
|
|
123
143
|
logger.error(f"Current app_id: {app_id}")
|
|
124
144
|
# Provide a clearer error message without the confusing "Provide valid app ID" message
|
|
125
145
|
return {
|
|
126
|
-
"error":
|
|
127
|
-
"details": {
|
|
146
|
+
"error": {
|
|
128
147
|
"message": "Meta API authentication configuration issue. Please check your app credentials.",
|
|
129
148
|
"original_error": error_obj.get("message"),
|
|
130
149
|
"code": error_obj.get("code")
|
|
@@ -132,11 +151,28 @@ async def make_api_request(
|
|
|
132
151
|
}
|
|
133
152
|
auth_manager.invalidate_token()
|
|
134
153
|
|
|
135
|
-
|
|
154
|
+
# Include full details for technical users
|
|
155
|
+
full_response = {
|
|
156
|
+
"headers": dict(e.response.headers),
|
|
157
|
+
"status_code": e.response.status_code,
|
|
158
|
+
"url": str(e.response.url),
|
|
159
|
+
"reason": getattr(e.response, "reason_phrase", "Unknown reason"),
|
|
160
|
+
"request_method": e.request.method,
|
|
161
|
+
"request_url": str(e.request.url)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
# Return a properly structured error object
|
|
165
|
+
return {
|
|
166
|
+
"error": {
|
|
167
|
+
"message": f"HTTP Error: {e.response.status_code}",
|
|
168
|
+
"details": error_info,
|
|
169
|
+
"full_response": full_response
|
|
170
|
+
}
|
|
171
|
+
}
|
|
136
172
|
|
|
137
173
|
except Exception as e:
|
|
138
174
|
logger.error(f"Request Error: {str(e)}")
|
|
139
|
-
return {"error": str(e)}
|
|
175
|
+
return {"error": {"message": str(e)}}
|
|
140
176
|
|
|
141
177
|
|
|
142
178
|
# Generic wrapper for all Meta API tools
|
|
@@ -174,12 +210,14 @@ def meta_api_tool(func):
|
|
|
174
210
|
logger.warning("No access token available, authentication needed")
|
|
175
211
|
auth_url = auth_manager.get_auth_url()
|
|
176
212
|
return json.dumps({
|
|
177
|
-
"error":
|
|
178
|
-
|
|
179
|
-
"
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
213
|
+
"error": {
|
|
214
|
+
"message": "Authentication Required",
|
|
215
|
+
"details": {
|
|
216
|
+
"description": "You need to authenticate with the Meta API before using this tool",
|
|
217
|
+
"action_required": "Please authenticate first",
|
|
218
|
+
"auth_url": auth_url,
|
|
219
|
+
"markdown_link": f"[Click here to authenticate with Meta Ads API]({auth_url})"
|
|
220
|
+
}
|
|
183
221
|
}
|
|
184
222
|
}, indent=2)
|
|
185
223
|
|
|
@@ -200,18 +238,24 @@ def meta_api_tool(func):
|
|
|
200
238
|
logger.error(f"Current app_id: {app_id}")
|
|
201
239
|
# Replace the confusing error with a more user-friendly one
|
|
202
240
|
return json.dumps({
|
|
203
|
-
"error":
|
|
204
|
-
|
|
205
|
-
"
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
241
|
+
"error": {
|
|
242
|
+
"message": "Meta API Configuration Issue",
|
|
243
|
+
"details": {
|
|
244
|
+
"description": "Your Meta API app is not properly configured",
|
|
245
|
+
"action_required": "Check your META_APP_ID environment variable",
|
|
246
|
+
"current_app_id": app_id,
|
|
247
|
+
"original_error": error_obj.get("message")
|
|
248
|
+
}
|
|
209
249
|
}
|
|
210
250
|
}, indent=2)
|
|
211
251
|
except Exception:
|
|
212
|
-
# Not JSON or other parsing error,
|
|
213
|
-
|
|
214
|
-
|
|
252
|
+
# Not JSON or other parsing error, wrap it in a dictionary
|
|
253
|
+
return json.dumps({"data": result}, indent=2)
|
|
254
|
+
|
|
255
|
+
# If result is already a dictionary, ensure it's properly serialized
|
|
256
|
+
if isinstance(result, dict):
|
|
257
|
+
return json.dumps(result, indent=2)
|
|
258
|
+
|
|
215
259
|
return result
|
|
216
260
|
except Exception as e:
|
|
217
261
|
logger.error(f"Error in {func.__name__}: {str(e)}")
|