meta-ads-mcp 0.4.3__tar.gz → 0.4.5__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.4.3 → meta_ads_mcp-0.4.5}/PKG-INFO +4 -2
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/README.md +3 -1
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/STREAMABLE_HTTP_SETUP.md +13 -13
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/__init__.py +1 -1
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/core/adsets.py +24 -69
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/core/auth.py +3 -1
- meta_ads_mcp-0.4.5/meta_ads_mcp/core/callback_server.py +257 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/pyproject.toml +1 -1
- meta_ads_mcp-0.4.3/meta_ads_mcp/core/callback_server.py +0 -1021
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/.github/workflows/publish.yml +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/.github/workflows/test.yml +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/.gitignore +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/CUSTOM_META_APP.md +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/Dockerfile +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/LICENSE +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/LOCAL_INSTALLATION.md +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/META_API_NOTES.md +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/RELEASE.md +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/examples/README.md +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/examples/example_http_client.py +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/future_improvements.md +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/images/meta-ads-example.png +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_auth.sh +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/__main__.py +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/core/__init__.py +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/core/accounts.py +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/core/ads.py +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/core/ads_library.py +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/core/api.py +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/core/authentication.py +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/core/budget_schedules.py +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/core/campaigns.py +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/core/duplication.py +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/core/http_auth_integration.py +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/core/insights.py +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/core/pipeboard_auth.py +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/core/reports.py +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/core/resources.py +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/core/server.py +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/core/utils.py +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/requirements.txt +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/setup.py +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/smithery.yaml +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/tests/README.md +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/tests/README_REGRESSION_TESTS.md +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/tests/__init__.py +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/tests/conftest.py +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/tests/test_duplication.py +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/tests/test_duplication_regression.py +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/tests/test_http_transport.py +0 -0
- {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/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.
|
|
3
|
+
Version: 0.4.5
|
|
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
|
|
@@ -49,7 +49,7 @@ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server for in
|
|
|
49
49
|
|
|
50
50
|
## Getting started with Remote MCP (Recommended)
|
|
51
51
|
|
|
52
|
-
The fastest and most reliable way to get started is to **[🚀 Get started with our Meta Ads Remote MCP](https://pipeboard.co)**. No technical setup required - just connect and start analyzing your ad campaigns with AI!
|
|
52
|
+
The fastest and most reliable way to get started is to **[🚀 Get started with our Meta Ads Remote MCP](https://pipeboard.co)**. Our cloud service uses streamable HTTP transport for reliable, scalable access to Meta Ads data. No technical setup required - just connect and start analyzing your ad campaigns with AI!
|
|
53
53
|
|
|
54
54
|
### For Claude Pro/Max Users
|
|
55
55
|
|
|
@@ -88,6 +88,8 @@ Use the Remote MCP URL: `https://mcp.pipeboard.co/meta-ads-mcp`
|
|
|
88
88
|
|
|
89
89
|
If you're a developer or need to customize the installation, you can run Meta Ads MCP locally. **Most marketers should use the Remote MCP above instead!** For complete technical setup instructions, see our **[Local Installation Guide](LOCAL_INSTALLATION.md)**.
|
|
90
90
|
|
|
91
|
+
Meta Ads MCP also supports **streamable HTTP transport**, allowing you to run it as a standalone HTTP API for web applications and custom integrations. See **[Streamable HTTP Setup Guide](STREAMABLE_HTTP_SETUP.md)** for complete instructions.
|
|
92
|
+
|
|
91
93
|
### Quick Local Setup
|
|
92
94
|
|
|
93
95
|
```bash
|
|
@@ -24,7 +24,7 @@ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server for in
|
|
|
24
24
|
|
|
25
25
|
## Getting started with Remote MCP (Recommended)
|
|
26
26
|
|
|
27
|
-
The fastest and most reliable way to get started is to **[🚀 Get started with our Meta Ads Remote MCP](https://pipeboard.co)**. No technical setup required - just connect and start analyzing your ad campaigns with AI!
|
|
27
|
+
The fastest and most reliable way to get started is to **[🚀 Get started with our Meta Ads Remote MCP](https://pipeboard.co)**. Our cloud service uses streamable HTTP transport for reliable, scalable access to Meta Ads data. No technical setup required - just connect and start analyzing your ad campaigns with AI!
|
|
28
28
|
|
|
29
29
|
### For Claude Pro/Max Users
|
|
30
30
|
|
|
@@ -63,6 +63,8 @@ Use the Remote MCP URL: `https://mcp.pipeboard.co/meta-ads-mcp`
|
|
|
63
63
|
|
|
64
64
|
If you're a developer or need to customize the installation, you can run Meta Ads MCP locally. **Most marketers should use the Remote MCP above instead!** For complete technical setup instructions, see our **[Local Installation Guide](LOCAL_INSTALLATION.md)**.
|
|
65
65
|
|
|
66
|
+
Meta Ads MCP also supports **streamable HTTP transport**, allowing you to run it as a standalone HTTP API for web applications and custom integrations. See **[Streamable HTTP Setup Guide](STREAMABLE_HTTP_SETUP.md)** for complete instructions.
|
|
67
|
+
|
|
66
68
|
### Quick Local Setup
|
|
67
69
|
|
|
68
70
|
```bash
|
|
@@ -26,10 +26,10 @@ export PIPEBOARD_API_TOKEN=your_pipeboard_token
|
|
|
26
26
|
|
|
27
27
|
### 3. Make HTTP Requests
|
|
28
28
|
|
|
29
|
-
The server accepts JSON-RPC 2.0 requests at the `/mcp
|
|
29
|
+
The server accepts JSON-RPC 2.0 requests at the `/mcp` endpoint. Use the `Authorization` header to provide your token.
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
|
-
curl -X POST http://localhost:8080/mcp
|
|
32
|
+
curl -X POST http://localhost:8080/mcp \
|
|
33
33
|
-H "Content-Type: application/json" \
|
|
34
34
|
-H "Accept: application/json, text/event-stream" \
|
|
35
35
|
-H "Authorization: Bearer your_pipeboard_token" \
|
|
@@ -77,7 +77,7 @@ python -m meta_ads_mcp --transport streamable-http --port 9000
|
|
|
77
77
|
|
|
78
78
|
```bash
|
|
79
79
|
curl -H "Authorization: Bearer your_pipeboard_token" \
|
|
80
|
-
-X POST http://localhost:8080/mcp
|
|
80
|
+
-X POST http://localhost:8080/mcp \
|
|
81
81
|
-H "Content-Type: application/json" \
|
|
82
82
|
-H "Accept: application/json, text/event-stream" \
|
|
83
83
|
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
|
|
@@ -89,7 +89,7 @@ If you have a Meta Developer App, you can use a direct access token via the `X-M
|
|
|
89
89
|
|
|
90
90
|
```bash
|
|
91
91
|
curl -H "X-META-ACCESS-TOKEN: your_meta_access_token" \
|
|
92
|
-
-X POST http://localhost:8080/mcp
|
|
92
|
+
-X POST http://localhost:8080/mcp \
|
|
93
93
|
-H "Content-Type: application/json" \
|
|
94
94
|
-H "Accept: application/json, text/event-stream" \
|
|
95
95
|
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
|
|
@@ -100,7 +100,7 @@ curl -H "X-META-ACCESS-TOKEN: your_meta_access_token" \
|
|
|
100
100
|
### Server URL Structure
|
|
101
101
|
|
|
102
102
|
**Base URL**: `http://localhost:8080`
|
|
103
|
-
**MCP Endpoint**: `/mcp
|
|
103
|
+
**MCP Endpoint**: `/mcp`
|
|
104
104
|
|
|
105
105
|
### MCP Protocol Methods
|
|
106
106
|
|
|
@@ -129,7 +129,7 @@ All responses follow JSON-RPC 2.0 format:
|
|
|
129
129
|
### 1. Initialize Session
|
|
130
130
|
|
|
131
131
|
```bash
|
|
132
|
-
curl -X POST http://localhost:8080/mcp
|
|
132
|
+
curl -X POST http://localhost:8080/mcp \
|
|
133
133
|
-H "Content-Type: application/json" \
|
|
134
134
|
-H "Accept: application/json, text/event-stream" \
|
|
135
135
|
-H "Authorization: Bearer your_token" \
|
|
@@ -148,7 +148,7 @@ curl -X POST http://localhost:8080/mcp/ \
|
|
|
148
148
|
### 2. List Available Tools
|
|
149
149
|
|
|
150
150
|
```bash
|
|
151
|
-
curl -X POST http://localhost:8080/mcp
|
|
151
|
+
curl -X POST http://localhost:8080/mcp \
|
|
152
152
|
-H "Content-Type: application/json" \
|
|
153
153
|
-H "Accept: application/json, text/event-stream" \
|
|
154
154
|
-H "Authorization: Bearer your_token" \
|
|
@@ -162,7 +162,7 @@ curl -X POST http://localhost:8080/mcp/ \
|
|
|
162
162
|
### 3. Get Ad Accounts
|
|
163
163
|
|
|
164
164
|
```bash
|
|
165
|
-
curl -X POST http://localhost:8080/mcp
|
|
165
|
+
curl -X POST http://localhost:8080/mcp \
|
|
166
166
|
-H "Content-Type: application/json" \
|
|
167
167
|
-H "Accept: application/json, text/event-stream" \
|
|
168
168
|
-H "Authorization: Bearer your_token" \
|
|
@@ -180,7 +180,7 @@ curl -X POST http://localhost:8080/mcp/ \
|
|
|
180
180
|
### 4. Get Campaign Performance
|
|
181
181
|
|
|
182
182
|
```bash
|
|
183
|
-
curl -X POST http://localhost:8080/mcp
|
|
183
|
+
curl -X POST http://localhost:8080/mcp \
|
|
184
184
|
-H "Content-Type: application/json" \
|
|
185
185
|
-H "Accept: application/json, text/event-stream" \
|
|
186
186
|
-H "Authorization: Bearer your_token" \
|
|
@@ -210,7 +210,7 @@ import json
|
|
|
210
210
|
class MetaAdsMCPClient:
|
|
211
211
|
def __init__(self, base_url="http://localhost:8080", token=None):
|
|
212
212
|
self.base_url = base_url
|
|
213
|
-
self.endpoint = f"{base_url}/mcp
|
|
213
|
+
self.endpoint = f"{base_url}/mcp"
|
|
214
214
|
self.headers = {
|
|
215
215
|
"Content-Type": "application/json",
|
|
216
216
|
"Accept": "application/json, text/event-stream"
|
|
@@ -245,7 +245,7 @@ const axios = require('axios');
|
|
|
245
245
|
class MetaAdsMCPClient {
|
|
246
246
|
constructor(baseUrl = 'http://localhost:8080', token = null) {
|
|
247
247
|
this.baseUrl = baseUrl;
|
|
248
|
-
this.endpoint = `${baseUrl}/mcp
|
|
248
|
+
this.endpoint = `${baseUrl}/mcp`;
|
|
249
249
|
this.headers = {
|
|
250
250
|
'Content-Type': 'application/json',
|
|
251
251
|
'Accept': 'application/json, text/event-stream'
|
|
@@ -325,7 +325,7 @@ export META_ACCESS_TOKEN=your_access_token
|
|
|
325
325
|
|
|
326
326
|
1. **Connection Refused**: Ensure the server is running and accessible on the specified port.
|
|
327
327
|
2. **Authentication Failed**: Verify your Bearer token is valid and included in the `Authorization` header.
|
|
328
|
-
3. **404 Not Found**: Make sure you're using the correct endpoint (`/mcp
|
|
328
|
+
3. **404 Not Found**: Make sure you're using the correct endpoint (`/mcp`).
|
|
329
329
|
4. **JSON-RPC Errors**: Check that your request follows the JSON-RPC 2.0 format.
|
|
330
330
|
|
|
331
331
|
### Debug Mode
|
|
@@ -337,7 +337,7 @@ Enable verbose logging by setting the log level in your environment if the appli
|
|
|
337
337
|
Test if the server is running by sending a `tools/list` request:
|
|
338
338
|
|
|
339
339
|
```bash
|
|
340
|
-
curl -X POST http://localhost:8080/mcp
|
|
340
|
+
curl -X POST http://localhost:8080/mcp \
|
|
341
341
|
-H "Content-Type: application/json" \
|
|
342
342
|
-H "Accept: application/json, text/event-stream" \
|
|
343
343
|
-H "Authorization: Bearer your_token" \
|
|
@@ -5,9 +5,6 @@ from typing import Optional, Dict, Any, List
|
|
|
5
5
|
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
|
-
import asyncio
|
|
9
|
-
from .callback_server import start_callback_server, shutdown_callback_server, update_confirmation
|
|
10
|
-
import urllib.parse
|
|
11
8
|
|
|
12
9
|
|
|
13
10
|
@mcp_server.tool()
|
|
@@ -211,94 +208,52 @@ async def update_adset(adset_id: str, frequency_control_specs: List[Dict[str, An
|
|
|
211
208
|
bid_strategy: Bid strategy (e.g., 'LOWEST_COST_WITH_BID_CAP')
|
|
212
209
|
bid_amount: Bid amount in account currency (in cents for USD)
|
|
213
210
|
status: Update ad set status (ACTIVE, PAUSED, etc.)
|
|
214
|
-
targeting:
|
|
215
|
-
(e.g. {"targeting_automation":{"advantage_audience":1}})
|
|
211
|
+
targeting: Complete targeting specifications (will replace existing targeting)
|
|
212
|
+
(e.g. {"targeting_automation":{"advantage_audience":1}, "geo_locations": {"countries": ["US"]}})
|
|
216
213
|
optimization_goal: Conversion optimization goal (e.g., 'LINK_CLICKS', 'CONVERSIONS', 'APP_INSTALLS', etc.)
|
|
217
214
|
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
218
215
|
"""
|
|
219
216
|
if not adset_id:
|
|
220
217
|
return json.dumps({"error": "No ad set ID provided"}, indent=2)
|
|
221
218
|
|
|
222
|
-
|
|
219
|
+
params = {}
|
|
223
220
|
|
|
224
221
|
if frequency_control_specs is not None:
|
|
225
|
-
|
|
222
|
+
params['frequency_control_specs'] = frequency_control_specs
|
|
226
223
|
|
|
227
224
|
if bid_strategy is not None:
|
|
228
|
-
|
|
225
|
+
params['bid_strategy'] = bid_strategy
|
|
229
226
|
|
|
230
227
|
if bid_amount is not None:
|
|
231
|
-
|
|
228
|
+
params['bid_amount'] = str(bid_amount)
|
|
232
229
|
|
|
233
230
|
if status is not None:
|
|
234
|
-
|
|
231
|
+
params['status'] = status
|
|
235
232
|
|
|
236
233
|
if optimization_goal is not None:
|
|
237
|
-
|
|
234
|
+
params['optimization_goal'] = optimization_goal
|
|
238
235
|
|
|
239
236
|
if targeting is not None:
|
|
240
|
-
#
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
# Check if the current ad set has targeting information
|
|
245
|
-
current_targeting = current_details.get('targeting', {})
|
|
246
|
-
|
|
247
|
-
if 'targeting_automation' in targeting:
|
|
248
|
-
# Only update targeting_automation while preserving other targeting settings
|
|
249
|
-
if current_targeting:
|
|
250
|
-
merged_targeting = current_targeting.copy()
|
|
251
|
-
merged_targeting['targeting_automation'] = targeting['targeting_automation']
|
|
252
|
-
changes['targeting'] = merged_targeting
|
|
253
|
-
else:
|
|
254
|
-
# If there's no existing targeting, we need to create a basic one
|
|
255
|
-
# Meta requires at least a geo_locations setting
|
|
256
|
-
basic_targeting = {
|
|
257
|
-
'targeting_automation': targeting['targeting_automation'],
|
|
258
|
-
'geo_locations': {'countries': ['US']} # Using US as default location
|
|
259
|
-
}
|
|
260
|
-
changes['targeting'] = basic_targeting
|
|
237
|
+
# Ensure proper JSON encoding for targeting
|
|
238
|
+
if isinstance(targeting, dict):
|
|
239
|
+
params['targeting'] = json.dumps(targeting)
|
|
261
240
|
else:
|
|
262
|
-
|
|
263
|
-
changes['targeting'] = targeting
|
|
241
|
+
params['targeting'] = targeting # Already a string
|
|
264
242
|
|
|
265
|
-
if not
|
|
243
|
+
if not params:
|
|
266
244
|
return json.dumps({"error": "No update parameters provided"}, indent=2)
|
|
245
|
+
|
|
246
|
+
endpoint = f"{adset_id}"
|
|
267
247
|
|
|
268
|
-
# Get current ad set details for comparison
|
|
269
|
-
current_details_json = await get_adset_details(adset_id=adset_id, access_token=access_token)
|
|
270
|
-
current_details = json.loads(current_details_json)
|
|
271
|
-
|
|
272
|
-
# Start the callback server if not already running
|
|
273
248
|
try:
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
changes_json = json.dumps(changes)
|
|
278
|
-
encoded_changes = urllib.parse.quote(changes_json)
|
|
279
|
-
confirmation_url = f"http://localhost:{port}/confirm-update?adset_id={adset_id}&token={access_token}&changes={encoded_changes}"
|
|
249
|
+
# Use POST method for updates as per Meta API documentation
|
|
250
|
+
data = await make_api_request(endpoint, access_token, params, method="POST")
|
|
251
|
+
return json.dumps(data, indent=2)
|
|
280
252
|
except Exception as e:
|
|
253
|
+
error_msg = str(e)
|
|
254
|
+
# Include adset_id in error for better context
|
|
281
255
|
return json.dumps({
|
|
282
|
-
"error": "
|
|
283
|
-
"
|
|
284
|
-
"
|
|
285
|
-
|
|
286
|
-
"proposed_changes": changes
|
|
287
|
-
}, indent=2)
|
|
288
|
-
|
|
289
|
-
# Reset the update confirmation
|
|
290
|
-
update_confirmation.clear()
|
|
291
|
-
update_confirmation.update({"approved": False})
|
|
292
|
-
|
|
293
|
-
# Return the confirmation link
|
|
294
|
-
response = {
|
|
295
|
-
"message": "Please confirm the ad set update",
|
|
296
|
-
"confirmation_url": confirmation_url,
|
|
297
|
-
"markdown_link": f"[Click here to confirm ad set update]({confirmation_url})",
|
|
298
|
-
"current_details": current_details,
|
|
299
|
-
"proposed_changes": changes,
|
|
300
|
-
"instructions_for_llm": "You must present this link as clickable Markdown to the user using the markdown_link format provided.",
|
|
301
|
-
"note": "Click the link to confirm and apply your ad set updates. Refresh the browser page if it doesn't load immediately."
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
return json.dumps(response, indent=2)
|
|
256
|
+
"error": f"Failed to update ad set {adset_id}",
|
|
257
|
+
"details": error_msg,
|
|
258
|
+
"params_sent": params
|
|
259
|
+
}, indent=2)
|
|
@@ -23,7 +23,9 @@ from .callback_server import (
|
|
|
23
23
|
from .pipeboard_auth import pipeboard_auth_manager
|
|
24
24
|
|
|
25
25
|
# Auth constants
|
|
26
|
-
|
|
26
|
+
# Scope includes pages_show_list and pages_read_engagement to fix issue #16
|
|
27
|
+
# where get_account_pages failed for regular users due to missing page permissions
|
|
28
|
+
AUTH_SCOPE = "ads_management,ads_read,business_management,public_profile,pages_show_list,pages_read_engagement"
|
|
27
29
|
AUTH_REDIRECT_URI = "http://localhost:8888/callback"
|
|
28
30
|
AUTH_RESPONSE_TYPE = "token"
|
|
29
31
|
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""Callback server for Meta Ads API authentication."""
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
import socket
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import webbrowser
|
|
9
|
+
import os
|
|
10
|
+
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
11
|
+
from urllib.parse import urlparse, parse_qs, quote
|
|
12
|
+
from typing import Dict, Any, Optional
|
|
13
|
+
|
|
14
|
+
from .utils import logger
|
|
15
|
+
|
|
16
|
+
# Global token container for communication between threads
|
|
17
|
+
token_container = {"token": None, "expires_in": None, "user_id": None}
|
|
18
|
+
|
|
19
|
+
# Global variables for server thread and state
|
|
20
|
+
callback_server_thread = None
|
|
21
|
+
callback_server_lock = threading.Lock()
|
|
22
|
+
callback_server_running = False
|
|
23
|
+
callback_server_port = None
|
|
24
|
+
callback_server_instance = None
|
|
25
|
+
server_shutdown_timer = None
|
|
26
|
+
|
|
27
|
+
# Timeout in seconds before shutting down the callback server
|
|
28
|
+
CALLBACK_SERVER_TIMEOUT = 180 # 3 minutes timeout
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class CallbackHandler(BaseHTTPRequestHandler):
|
|
32
|
+
def do_GET(self):
|
|
33
|
+
try:
|
|
34
|
+
# Print path for debugging
|
|
35
|
+
print(f"Callback server received request: {self.path}")
|
|
36
|
+
|
|
37
|
+
if self.path.startswith("/callback"):
|
|
38
|
+
self._handle_oauth_callback()
|
|
39
|
+
elif self.path.startswith("/token"):
|
|
40
|
+
self._handle_token()
|
|
41
|
+
else:
|
|
42
|
+
# If no matching path, return a 404 error
|
|
43
|
+
self.send_response(404)
|
|
44
|
+
self.end_headers()
|
|
45
|
+
except Exception as e:
|
|
46
|
+
print(f"Error processing request: {e}")
|
|
47
|
+
self.send_response(500)
|
|
48
|
+
self.end_headers()
|
|
49
|
+
|
|
50
|
+
def _handle_oauth_callback(self):
|
|
51
|
+
"""Handle OAuth callback after user authorization"""
|
|
52
|
+
# Check if we're being redirected from Facebook with an authorization code
|
|
53
|
+
parsed_url = urlparse(self.path)
|
|
54
|
+
params = parse_qs(parsed_url.query)
|
|
55
|
+
|
|
56
|
+
# Check for code parameter
|
|
57
|
+
code = params.get('code', [None])[0]
|
|
58
|
+
state = params.get('state', [None])[0]
|
|
59
|
+
error = params.get('error', [None])[0]
|
|
60
|
+
|
|
61
|
+
# Send 200 OK response with a simple HTML page
|
|
62
|
+
self.send_response(200)
|
|
63
|
+
self.send_header("Content-type", "text/html")
|
|
64
|
+
self.end_headers()
|
|
65
|
+
|
|
66
|
+
if error:
|
|
67
|
+
# User denied access or other error occurred
|
|
68
|
+
html = f"""
|
|
69
|
+
<html>
|
|
70
|
+
<head><title>Authorization Failed</title></head>
|
|
71
|
+
<body>
|
|
72
|
+
<h1>Authorization Failed</h1>
|
|
73
|
+
<p>Error: {error}</p>
|
|
74
|
+
<p>The authorization was cancelled or failed. You can close this window.</p>
|
|
75
|
+
</body>
|
|
76
|
+
</html>
|
|
77
|
+
"""
|
|
78
|
+
logger.error(f"OAuth authorization failed: {error}")
|
|
79
|
+
elif code:
|
|
80
|
+
# Success case - we have the authorization code
|
|
81
|
+
logger.info(f"Received authorization code: {code[:10]}...")
|
|
82
|
+
|
|
83
|
+
# Store the authorization code temporarily
|
|
84
|
+
# The auth module will exchange this for an access token
|
|
85
|
+
token_container.update({
|
|
86
|
+
"auth_code": code,
|
|
87
|
+
"state": state,
|
|
88
|
+
"timestamp": asyncio.get_event_loop().time()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
html = """
|
|
92
|
+
<html>
|
|
93
|
+
<head><title>Authorization Successful</title></head>
|
|
94
|
+
<body>
|
|
95
|
+
<h1>✅ Authorization Successful!</h1>
|
|
96
|
+
<p>You have successfully authorized the Meta Ads MCP application.</p>
|
|
97
|
+
<p>You can now close this window and return to your application.</p>
|
|
98
|
+
<script>
|
|
99
|
+
// Try to close the window automatically after 2 seconds
|
|
100
|
+
setTimeout(function() {
|
|
101
|
+
window.close();
|
|
102
|
+
}, 2000);
|
|
103
|
+
</script>
|
|
104
|
+
</body>
|
|
105
|
+
</html>
|
|
106
|
+
"""
|
|
107
|
+
logger.info("OAuth authorization successful")
|
|
108
|
+
else:
|
|
109
|
+
# No code or error - something unexpected happened
|
|
110
|
+
html = """
|
|
111
|
+
<html>
|
|
112
|
+
<head><title>Unexpected Response</title></head>
|
|
113
|
+
<body>
|
|
114
|
+
<h1>Unexpected Response</h1>
|
|
115
|
+
<p>No authorization code or error received. Please try again.</p>
|
|
116
|
+
</body>
|
|
117
|
+
</html>
|
|
118
|
+
"""
|
|
119
|
+
logger.warning("OAuth callback received without code or error")
|
|
120
|
+
|
|
121
|
+
self.wfile.write(html.encode())
|
|
122
|
+
|
|
123
|
+
def _handle_token(self):
|
|
124
|
+
"""Handle token endpoint for retrieving stored token data"""
|
|
125
|
+
# This endpoint allows other parts of the application to retrieve
|
|
126
|
+
# token information from the callback server
|
|
127
|
+
|
|
128
|
+
self.send_response(200)
|
|
129
|
+
self.send_header("Content-type", "application/json")
|
|
130
|
+
self.end_headers()
|
|
131
|
+
|
|
132
|
+
# Return current token container contents
|
|
133
|
+
response_data = {
|
|
134
|
+
"status": "success",
|
|
135
|
+
"data": token_container
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
self.wfile.write(json.dumps(response_data).encode())
|
|
139
|
+
|
|
140
|
+
# The actual token processing is now handled by the auth module
|
|
141
|
+
# that imports this module and accesses token_container
|
|
142
|
+
|
|
143
|
+
# Silence server logs
|
|
144
|
+
def log_message(self, format, *args):
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def shutdown_callback_server():
|
|
149
|
+
"""
|
|
150
|
+
Shutdown the callback server if it's running
|
|
151
|
+
"""
|
|
152
|
+
global callback_server_thread, callback_server_running, callback_server_port, callback_server_instance, server_shutdown_timer
|
|
153
|
+
|
|
154
|
+
with callback_server_lock:
|
|
155
|
+
if not callback_server_running:
|
|
156
|
+
print("Callback server is not running")
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
if server_shutdown_timer is not None:
|
|
160
|
+
server_shutdown_timer.cancel()
|
|
161
|
+
server_shutdown_timer = None
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
if callback_server_instance:
|
|
165
|
+
print("Shutting down callback server...")
|
|
166
|
+
callback_server_instance.shutdown()
|
|
167
|
+
callback_server_instance.server_close()
|
|
168
|
+
print("Callback server shut down successfully")
|
|
169
|
+
|
|
170
|
+
if callback_server_thread and callback_server_thread.is_alive():
|
|
171
|
+
callback_server_thread.join(timeout=5)
|
|
172
|
+
if callback_server_thread.is_alive():
|
|
173
|
+
print("Warning: Callback server thread did not shut down cleanly")
|
|
174
|
+
except Exception as e:
|
|
175
|
+
print(f"Error during callback server shutdown: {e}")
|
|
176
|
+
finally:
|
|
177
|
+
callback_server_running = False
|
|
178
|
+
callback_server_thread = None
|
|
179
|
+
callback_server_port = None
|
|
180
|
+
callback_server_instance = None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def start_callback_server() -> int:
|
|
184
|
+
"""
|
|
185
|
+
Start the callback server and return the port number it's running on.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
int: Port number the server is listening on
|
|
189
|
+
|
|
190
|
+
Raises:
|
|
191
|
+
Exception: If the server fails to start
|
|
192
|
+
"""
|
|
193
|
+
global callback_server_thread, callback_server_running, callback_server_port, callback_server_instance, server_shutdown_timer
|
|
194
|
+
|
|
195
|
+
# Check if callback server is disabled
|
|
196
|
+
if os.environ.get("META_ADS_DISABLE_CALLBACK_SERVER"):
|
|
197
|
+
raise Exception("Callback server is disabled via META_ADS_DISABLE_CALLBACK_SERVER environment variable")
|
|
198
|
+
|
|
199
|
+
with callback_server_lock:
|
|
200
|
+
if callback_server_running:
|
|
201
|
+
print(f"Callback server already running on port {callback_server_port}")
|
|
202
|
+
return callback_server_port
|
|
203
|
+
|
|
204
|
+
# Find an available port
|
|
205
|
+
port = 8080
|
|
206
|
+
max_attempts = 10
|
|
207
|
+
for attempt in range(max_attempts):
|
|
208
|
+
try:
|
|
209
|
+
# Test if port is available
|
|
210
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
211
|
+
s.bind(('localhost', port))
|
|
212
|
+
break
|
|
213
|
+
except OSError:
|
|
214
|
+
port += 1
|
|
215
|
+
else:
|
|
216
|
+
raise Exception(f"Could not find an available port after {max_attempts} attempts")
|
|
217
|
+
|
|
218
|
+
callback_server_port = port
|
|
219
|
+
|
|
220
|
+
# Start the server in a separate thread
|
|
221
|
+
callback_server_thread = threading.Thread(target=server_thread, daemon=True)
|
|
222
|
+
callback_server_thread.start()
|
|
223
|
+
|
|
224
|
+
# Wait a moment for the server to start
|
|
225
|
+
import time
|
|
226
|
+
time.sleep(0.5)
|
|
227
|
+
|
|
228
|
+
if not callback_server_running:
|
|
229
|
+
raise Exception("Failed to start callback server")
|
|
230
|
+
|
|
231
|
+
# Set up automatic shutdown timer
|
|
232
|
+
def auto_shutdown():
|
|
233
|
+
print(f"Callback server auto-shutdown after {CALLBACK_SERVER_TIMEOUT} seconds")
|
|
234
|
+
shutdown_callback_server()
|
|
235
|
+
|
|
236
|
+
server_shutdown_timer = threading.Timer(CALLBACK_SERVER_TIMEOUT, auto_shutdown)
|
|
237
|
+
server_shutdown_timer.start()
|
|
238
|
+
|
|
239
|
+
print(f"Callback server started on http://localhost:{port}")
|
|
240
|
+
return port
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def server_thread():
|
|
244
|
+
"""Thread function to run the callback server"""
|
|
245
|
+
global callback_server_running, callback_server_instance
|
|
246
|
+
|
|
247
|
+
try:
|
|
248
|
+
callback_server_instance = HTTPServer(('localhost', callback_server_port), CallbackHandler)
|
|
249
|
+
callback_server_running = True
|
|
250
|
+
print(f"Callback server thread started on port {callback_server_port}")
|
|
251
|
+
callback_server_instance.serve_forever()
|
|
252
|
+
except Exception as e:
|
|
253
|
+
print(f"Callback server error: {e}")
|
|
254
|
+
callback_server_running = False
|
|
255
|
+
finally:
|
|
256
|
+
print("Callback server thread finished")
|
|
257
|
+
callback_server_running = False
|