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.
Files changed (51) hide show
  1. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/PKG-INFO +4 -2
  2. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/README.md +3 -1
  3. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/STREAMABLE_HTTP_SETUP.md +13 -13
  4. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/__init__.py +1 -1
  5. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/core/adsets.py +24 -69
  6. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/core/auth.py +3 -1
  7. meta_ads_mcp-0.4.5/meta_ads_mcp/core/callback_server.py +257 -0
  8. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/pyproject.toml +1 -1
  9. meta_ads_mcp-0.4.3/meta_ads_mcp/core/callback_server.py +0 -1021
  10. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/.github/workflows/publish.yml +0 -0
  11. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/.github/workflows/test.yml +0 -0
  12. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/.gitignore +0 -0
  13. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/CUSTOM_META_APP.md +0 -0
  14. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/Dockerfile +0 -0
  15. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/LICENSE +0 -0
  16. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/LOCAL_INSTALLATION.md +0 -0
  17. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/META_API_NOTES.md +0 -0
  18. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/RELEASE.md +0 -0
  19. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/examples/README.md +0 -0
  20. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/examples/example_http_client.py +0 -0
  21. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/future_improvements.md +0 -0
  22. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/images/meta-ads-example.png +0 -0
  23. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_auth.sh +0 -0
  24. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/__main__.py +0 -0
  25. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/core/__init__.py +0 -0
  26. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/core/accounts.py +0 -0
  27. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/core/ads.py +0 -0
  28. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/core/ads_library.py +0 -0
  29. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/core/api.py +0 -0
  30. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/core/authentication.py +0 -0
  31. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/core/budget_schedules.py +0 -0
  32. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/core/campaigns.py +0 -0
  33. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/core/duplication.py +0 -0
  34. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/core/http_auth_integration.py +0 -0
  35. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/core/insights.py +0 -0
  36. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/core/pipeboard_auth.py +0 -0
  37. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/core/reports.py +0 -0
  38. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/core/resources.py +0 -0
  39. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/core/server.py +0 -0
  40. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/meta_ads_mcp/core/utils.py +0 -0
  41. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/requirements.txt +0 -0
  42. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/setup.py +0 -0
  43. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/smithery.yaml +0 -0
  44. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/tests/README.md +0 -0
  45. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/tests/README_REGRESSION_TESTS.md +0 -0
  46. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/tests/__init__.py +0 -0
  47. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/tests/conftest.py +0 -0
  48. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/tests/test_duplication.py +0 -0
  49. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/tests/test_duplication_regression.py +0 -0
  50. {meta_ads_mcp-0.4.3 → meta_ads_mcp-0.4.5}/tests/test_http_transport.py +0 -0
  51. {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
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/` endpoint. Use the `Authorization` header to provide your token.
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" \
@@ -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.3"
10
+ __version__ = "0.4.5"
11
11
 
12
12
  __all__ = [
13
13
  'get_ad_accounts',
@@ -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: Targeting specifications including targeting_automation
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
- changes = {}
219
+ params = {}
223
220
 
224
221
  if frequency_control_specs is not None:
225
- changes['frequency_control_specs'] = frequency_control_specs
222
+ params['frequency_control_specs'] = frequency_control_specs
226
223
 
227
224
  if bid_strategy is not None:
228
- changes['bid_strategy'] = bid_strategy
225
+ params['bid_strategy'] = bid_strategy
229
226
 
230
227
  if bid_amount is not None:
231
- changes['bid_amount'] = bid_amount
228
+ params['bid_amount'] = str(bid_amount)
232
229
 
233
230
  if status is not None:
234
- changes['status'] = status
231
+ params['status'] = status
235
232
 
236
233
  if optimization_goal is not None:
237
- changes['optimization_goal'] = optimization_goal
234
+ params['optimization_goal'] = optimization_goal
238
235
 
239
236
  if targeting is not None:
240
- # Get current ad set details to preserve existing targeting settings
241
- current_details_json = await get_adset_details(adset_id=adset_id, access_token=access_token)
242
- current_details = json.loads(current_details_json)
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
- # Full targeting replacement
263
- changes['targeting'] = targeting
241
+ params['targeting'] = targeting # Already a string
264
242
 
265
- if not changes:
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
- port = start_callback_server()
275
-
276
- # Generate confirmation URL with properly encoded parameters
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": "Callback server disabled",
283
- "message": f"Cannot create confirmation URL: {str(e)}",
284
- "suggestion": "Manual update confirmation not available when META_ADS_DISABLE_CALLBACK_SERVER is set",
285
- "adset_id": adset_id,
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
- AUTH_SCOPE = "ads_management,ads_read,business_management,public_profile"
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "meta-ads-mcp"
7
- version = "0.4.3"
7
+ version = "0.4.5"
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"