meta-ads-mcp-python 1.0.79__py3-none-any.whl
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/__init__.py +79 -0
- meta_ads_mcp/__main__.py +10 -0
- meta_ads_mcp/core/__init__.py +55 -0
- meta_ads_mcp/core/accounts.py +141 -0
- meta_ads_mcp/core/ads.py +2751 -0
- meta_ads_mcp/core/ads_library.py +74 -0
- meta_ads_mcp/core/adsets.py +666 -0
- meta_ads_mcp/core/api.py +431 -0
- meta_ads_mcp/core/auth.py +567 -0
- meta_ads_mcp/core/authentication.py +207 -0
- meta_ads_mcp/core/budget_schedules.py +70 -0
- meta_ads_mcp/core/callback_server.py +256 -0
- meta_ads_mcp/core/campaigns.py +379 -0
- meta_ads_mcp/core/duplication.py +523 -0
- meta_ads_mcp/core/http_auth_integration.py +307 -0
- meta_ads_mcp/core/insights.py +161 -0
- meta_ads_mcp/core/mcc.py +232 -0
- meta_ads_mcp/core/openai_deep_research.py +418 -0
- meta_ads_mcp/core/pipeboard_auth.py +510 -0
- meta_ads_mcp/core/reports.py +135 -0
- meta_ads_mcp/core/resources.py +46 -0
- meta_ads_mcp/core/server.py +391 -0
- meta_ads_mcp/core/targeting.py +542 -0
- meta_ads_mcp/core/utils.py +225 -0
- meta_ads_mcp/settings.py +33 -0
- meta_ads_mcp_python-1.0.79.dist-info/METADATA +187 -0
- meta_ads_mcp_python-1.0.79.dist-info/RECORD +29 -0
- meta_ads_mcp_python-1.0.79.dist-info/WHEEL +4 -0
- meta_ads_mcp_python-1.0.79.dist-info/entry_points.txt +3 -0
meta_ads_mcp/core/api.py
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
"""Core API functionality for Meta Ads API."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional, Callable
|
|
4
|
+
import json
|
|
5
|
+
import hmac
|
|
6
|
+
import hashlib
|
|
7
|
+
import httpx
|
|
8
|
+
import asyncio
|
|
9
|
+
import functools
|
|
10
|
+
import os
|
|
11
|
+
from . import auth
|
|
12
|
+
from .auth import needs_authentication, auth_manager, start_callback_server, shutdown_callback_server
|
|
13
|
+
from .utils import logger
|
|
14
|
+
|
|
15
|
+
class McpToolError(Exception):
|
|
16
|
+
"""Base class for MCP tool errors that must set isError: true.
|
|
17
|
+
|
|
18
|
+
Subclasses should be raised (not returned) from tool handlers.
|
|
19
|
+
meta_api_tool re-raises these so FastMCP sees them and sets
|
|
20
|
+
isError: true in the JSON-RPC response, which triggers the usage
|
|
21
|
+
credit refund in the Next.js proxy.
|
|
22
|
+
"""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def ensure_act_prefix(account_id: str) -> str:
|
|
27
|
+
"""Ensure account_id has the 'act_' prefix required by Meta's Graph API."""
|
|
28
|
+
if account_id and not account_id.startswith("act_"):
|
|
29
|
+
return f"act_{account_id}"
|
|
30
|
+
return account_id
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Constants
|
|
34
|
+
META_GRAPH_API_VERSION = "v24.0"
|
|
35
|
+
META_GRAPH_API_BASE = f"https://graph.facebook.com/{META_GRAPH_API_VERSION}"
|
|
36
|
+
USER_AGENT = "meta-ads-mcp/1.0"
|
|
37
|
+
|
|
38
|
+
# Log key environment and configuration at startup
|
|
39
|
+
logger.info("Core API module initialized")
|
|
40
|
+
logger.info(f"Graph API Version: {META_GRAPH_API_VERSION}")
|
|
41
|
+
logger.info(f"META_APP_ID env var present: {'Yes' if os.environ.get('META_APP_ID') else 'No'}")
|
|
42
|
+
logger.info(f"META_APP_SECRET env var present (appsecret_proof will be {'enabled' if os.environ.get('META_APP_SECRET') else 'disabled'})")
|
|
43
|
+
|
|
44
|
+
class GraphAPIError(Exception):
|
|
45
|
+
"""Exception raised for errors from the Graph API."""
|
|
46
|
+
def __init__(self, error_data: Dict[str, Any]):
|
|
47
|
+
self.error_data = error_data
|
|
48
|
+
self.message = error_data.get('message', 'Unknown Graph API error')
|
|
49
|
+
super().__init__(self.message)
|
|
50
|
+
|
|
51
|
+
# Log error details
|
|
52
|
+
logger.error(f"Graph API Error: {self.message}")
|
|
53
|
+
logger.debug(f"Error details: {error_data}")
|
|
54
|
+
|
|
55
|
+
# Check if this is an auth error (code 4 is rate limiting, NOT auth)
|
|
56
|
+
if "code" in error_data and error_data["code"] in [190, 102]:
|
|
57
|
+
logger.warning(f"Auth error detected (code: {error_data['code']}). Invalidating token.")
|
|
58
|
+
auth_manager.invalidate_token()
|
|
59
|
+
elif "code" in error_data and error_data["code"] == 4:
|
|
60
|
+
logger.warning(f"Rate limit error detected (code: 4, subcode: {error_data.get('error_subcode', 'N/A')}). Token is still valid -- NOT invalidating.")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _log_meta_rate_limit_headers(headers: dict, endpoint: str) -> None:
|
|
64
|
+
"""Log Meta's rate limit headers for observability (X-App-Usage, X-Business-Use-Case-Usage)."""
|
|
65
|
+
app_usage = headers.get("x-app-usage")
|
|
66
|
+
biz_usage = headers.get("x-business-use-case-usage")
|
|
67
|
+
ad_account_usage = headers.get("x-ad-account-usage")
|
|
68
|
+
|
|
69
|
+
if app_usage or biz_usage or ad_account_usage:
|
|
70
|
+
usage_data = {}
|
|
71
|
+
if app_usage:
|
|
72
|
+
try:
|
|
73
|
+
usage_data["app_usage"] = json.loads(app_usage)
|
|
74
|
+
except (json.JSONDecodeError, TypeError):
|
|
75
|
+
usage_data["app_usage_raw"] = str(app_usage)
|
|
76
|
+
if biz_usage:
|
|
77
|
+
try:
|
|
78
|
+
usage_data["business_use_case_usage"] = json.loads(biz_usage)
|
|
79
|
+
except (json.JSONDecodeError, TypeError):
|
|
80
|
+
usage_data["business_use_case_usage_raw"] = str(biz_usage)
|
|
81
|
+
if ad_account_usage:
|
|
82
|
+
try:
|
|
83
|
+
usage_data["ad_account_usage"] = json.loads(ad_account_usage)
|
|
84
|
+
except (json.JSONDecodeError, TypeError):
|
|
85
|
+
usage_data["ad_account_usage_raw"] = str(ad_account_usage)
|
|
86
|
+
|
|
87
|
+
# Warn at high usage levels (any field >= 80%)
|
|
88
|
+
is_high = False
|
|
89
|
+
for key, val in usage_data.items():
|
|
90
|
+
if isinstance(val, dict):
|
|
91
|
+
for metric, pct in val.items():
|
|
92
|
+
if isinstance(pct, (int, float)) and pct >= 80:
|
|
93
|
+
is_high = True
|
|
94
|
+
break
|
|
95
|
+
|
|
96
|
+
log_fn = logger.warning if is_high else logger.info
|
|
97
|
+
log_fn(f"meta_rate_limit_usage endpoint={endpoint} {json.dumps(usage_data)}")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
async def make_api_request(
|
|
101
|
+
endpoint: str,
|
|
102
|
+
access_token: str,
|
|
103
|
+
params: Optional[Dict[str, Any]] = None,
|
|
104
|
+
method: str = "GET"
|
|
105
|
+
) -> Dict[str, Any]:
|
|
106
|
+
"""
|
|
107
|
+
Make a request to the Meta Graph API.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
endpoint: API endpoint path (without base URL)
|
|
111
|
+
access_token: Meta API access token
|
|
112
|
+
params: Additional query parameters
|
|
113
|
+
method: HTTP method (GET, POST, DELETE)
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
API response as a dictionary
|
|
117
|
+
"""
|
|
118
|
+
# Validate access token before proceeding
|
|
119
|
+
if not access_token:
|
|
120
|
+
logger.error("API request attempted with blank access token")
|
|
121
|
+
return {
|
|
122
|
+
"error": {
|
|
123
|
+
"message": "Authentication Required",
|
|
124
|
+
"details": "A valid access token is required to access the Meta API",
|
|
125
|
+
"action_required": "Please authenticate first"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
url = f"{META_GRAPH_API_BASE}/{endpoint}"
|
|
130
|
+
|
|
131
|
+
headers = {
|
|
132
|
+
"User-Agent": USER_AGENT,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
request_params = params or {}
|
|
136
|
+
request_params["access_token"] = access_token
|
|
137
|
+
|
|
138
|
+
# Add appsecret_proof when META_APP_SECRET is configured.
|
|
139
|
+
# Required for system user tokens and recommended by Meta for all
|
|
140
|
+
# server-to-server API calls to verify token authenticity.
|
|
141
|
+
# See: https://developers.facebook.com/docs/graph-api/securing-requests/
|
|
142
|
+
app_secret = os.environ.get("META_APP_SECRET", "")
|
|
143
|
+
if app_secret and access_token:
|
|
144
|
+
request_params["appsecret_proof"] = hmac.new(
|
|
145
|
+
app_secret.encode("utf-8"),
|
|
146
|
+
access_token.encode("utf-8"),
|
|
147
|
+
hashlib.sha256,
|
|
148
|
+
).hexdigest()
|
|
149
|
+
|
|
150
|
+
# Logging the request (masking token for security)
|
|
151
|
+
masked_params = {k: "***MASKED***" if k in ("access_token", "appsecret_proof") else v for k, v in request_params.items()}
|
|
152
|
+
logger.debug(f"API Request: {method} {url}")
|
|
153
|
+
logger.debug(f"Request params: {masked_params}")
|
|
154
|
+
|
|
155
|
+
# Check for app_id in params
|
|
156
|
+
app_id = auth_manager.app_id
|
|
157
|
+
logger.debug(f"Current app_id from auth_manager: {app_id}")
|
|
158
|
+
|
|
159
|
+
async with httpx.AsyncClient() as client:
|
|
160
|
+
try:
|
|
161
|
+
if method == "GET":
|
|
162
|
+
# For GET, JSON-encode dict/list params (e.g., targeting_spec) to proper strings
|
|
163
|
+
encoded_params = {}
|
|
164
|
+
for key, value in request_params.items():
|
|
165
|
+
if isinstance(value, (dict, list)):
|
|
166
|
+
encoded_params[key] = json.dumps(value)
|
|
167
|
+
else:
|
|
168
|
+
encoded_params[key] = value
|
|
169
|
+
response = await client.get(url, params=encoded_params, headers=headers, timeout=30.0)
|
|
170
|
+
elif method == "POST":
|
|
171
|
+
# For Meta API, POST requests need data, not JSON
|
|
172
|
+
if 'targeting' in request_params and isinstance(request_params['targeting'], dict):
|
|
173
|
+
# Convert targeting dict to string for the API
|
|
174
|
+
request_params['targeting'] = json.dumps(request_params['targeting'])
|
|
175
|
+
|
|
176
|
+
# Convert lists and dicts to JSON strings
|
|
177
|
+
for key, value in request_params.items():
|
|
178
|
+
if isinstance(value, (list, dict)):
|
|
179
|
+
request_params[key] = json.dumps(value)
|
|
180
|
+
|
|
181
|
+
logger.debug(f"POST params (prepared): {masked_params}")
|
|
182
|
+
response = await client.post(url, data=request_params, headers=headers, timeout=30.0)
|
|
183
|
+
elif method == "PUT":
|
|
184
|
+
# PUT for updates that Meta requires via PUT (e.g., creative_features_spec).
|
|
185
|
+
# Meta expects access_token as a query param, not in the body.
|
|
186
|
+
query_params = {}
|
|
187
|
+
body_params = {}
|
|
188
|
+
for key, value in request_params.items():
|
|
189
|
+
if key in ("access_token", "appsecret_proof"):
|
|
190
|
+
query_params[key] = value
|
|
191
|
+
elif isinstance(value, (list, dict)):
|
|
192
|
+
body_params[key] = json.dumps(value)
|
|
193
|
+
else:
|
|
194
|
+
body_params[key] = value
|
|
195
|
+
response = await client.put(url, params=query_params, data=body_params, headers=headers, timeout=30.0)
|
|
196
|
+
elif method == "DELETE":
|
|
197
|
+
response = await client.delete(url, params=request_params, headers=headers, timeout=30.0)
|
|
198
|
+
else:
|
|
199
|
+
raise ValueError(f"Unsupported HTTP method: {method}")
|
|
200
|
+
|
|
201
|
+
response.raise_for_status()
|
|
202
|
+
logger.debug(f"API Response status: {response.status_code}")
|
|
203
|
+
|
|
204
|
+
# Log Meta rate limit headers for observability
|
|
205
|
+
_log_meta_rate_limit_headers(response.headers, endpoint)
|
|
206
|
+
|
|
207
|
+
# Ensure the response is JSON and return it as a dictionary
|
|
208
|
+
try:
|
|
209
|
+
return response.json()
|
|
210
|
+
except json.JSONDecodeError:
|
|
211
|
+
# If not JSON, return text content in a structured format
|
|
212
|
+
return {
|
|
213
|
+
"text_response": response.text,
|
|
214
|
+
"status_code": response.status_code
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
except httpx.HTTPStatusError as e:
|
|
218
|
+
error_info = {}
|
|
219
|
+
try:
|
|
220
|
+
error_info = e.response.json()
|
|
221
|
+
except:
|
|
222
|
+
error_info = {"status_code": e.response.status_code, "text": e.response.text}
|
|
223
|
+
|
|
224
|
+
logger.error(f"HTTP Error: {e.response.status_code} - {error_info}")
|
|
225
|
+
|
|
226
|
+
# Log Meta rate limit headers even on errors
|
|
227
|
+
_log_meta_rate_limit_headers(e.response.headers, endpoint)
|
|
228
|
+
|
|
229
|
+
# Check for rate limit errors vs authentication errors.
|
|
230
|
+
# Code 4 is a rate limit (NOT auth) -- do NOT invalidate token.
|
|
231
|
+
if "error" in error_info:
|
|
232
|
+
error_obj = error_info.get("error", {})
|
|
233
|
+
error_code = error_obj.get("code") if isinstance(error_obj, dict) else None
|
|
234
|
+
|
|
235
|
+
if error_code == 4:
|
|
236
|
+
# Application-level rate limit -- token is still valid
|
|
237
|
+
logger.warning(
|
|
238
|
+
f"Facebook API rate limit (code=4, subcode={error_obj.get('error_subcode', 'N/A')}, "
|
|
239
|
+
f"msg={error_obj.get('error_user_msg', error_obj.get('message', 'N/A'))}). "
|
|
240
|
+
f"Token is still valid -- NOT invalidating."
|
|
241
|
+
)
|
|
242
|
+
elif error_code in [190, 102, 200, 10]:
|
|
243
|
+
logger.warning(f"Detected Facebook API auth error: {error_code}")
|
|
244
|
+
if error_code == 200 and "Provide valid app ID" in error_obj.get("message", ""):
|
|
245
|
+
logger.error("Meta API authentication configuration issue")
|
|
246
|
+
logger.error(f"Current app_id: {app_id}")
|
|
247
|
+
return {
|
|
248
|
+
"error": {
|
|
249
|
+
"message": "Meta API authentication configuration issue. Please check your app credentials.",
|
|
250
|
+
"original_error": error_obj.get("message"),
|
|
251
|
+
"code": error_code
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
auth_manager.invalidate_token()
|
|
255
|
+
elif e.response.status_code in [401, 403]:
|
|
256
|
+
logger.warning(f"Detected authentication error ({e.response.status_code})")
|
|
257
|
+
auth_manager.invalidate_token()
|
|
258
|
+
elif e.response.status_code in [401, 403]:
|
|
259
|
+
logger.warning(f"Detected authentication error ({e.response.status_code})")
|
|
260
|
+
auth_manager.invalidate_token()
|
|
261
|
+
|
|
262
|
+
# Include full details for technical users
|
|
263
|
+
full_response = {
|
|
264
|
+
"headers": dict(e.response.headers),
|
|
265
|
+
"status_code": e.response.status_code,
|
|
266
|
+
"url": str(e.response.url),
|
|
267
|
+
"reason": getattr(e.response, "reason_phrase", "Unknown reason"),
|
|
268
|
+
"request_method": e.request.method,
|
|
269
|
+
"request_url": str(e.request.url)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
# Return a properly structured error object
|
|
273
|
+
return {
|
|
274
|
+
"error": {
|
|
275
|
+
"message": f"HTTP Error: {e.response.status_code}",
|
|
276
|
+
"details": error_info,
|
|
277
|
+
"full_response": full_response
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
except Exception as e:
|
|
282
|
+
logger.error(f"Request Error: {str(e)}")
|
|
283
|
+
return {"error": {"message": str(e)}}
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
# Generic wrapper for all Meta API tools
|
|
287
|
+
def meta_api_tool(func):
|
|
288
|
+
"""Decorator for Meta API tools that handles authentication and error handling."""
|
|
289
|
+
@functools.wraps(func)
|
|
290
|
+
async def wrapper(*args, **kwargs):
|
|
291
|
+
try:
|
|
292
|
+
# Log function call
|
|
293
|
+
logger.debug(f"Function call: {func.__name__}")
|
|
294
|
+
logger.debug(f"Args: {args}")
|
|
295
|
+
# Log kwargs without sensitive info
|
|
296
|
+
safe_kwargs = {k: ('***TOKEN***' if k == 'access_token' else v) for k, v in kwargs.items()}
|
|
297
|
+
logger.debug(f"Kwargs: {safe_kwargs}")
|
|
298
|
+
|
|
299
|
+
# Log app ID information
|
|
300
|
+
app_id = auth_manager.app_id
|
|
301
|
+
logger.debug(f"Current app_id: {app_id}")
|
|
302
|
+
logger.debug(f"META_APP_ID env var: {os.environ.get('META_APP_ID')}")
|
|
303
|
+
|
|
304
|
+
# If access_token is not in kwargs or not kwargs['access_token'], try to get it from auth_manager
|
|
305
|
+
if 'access_token' not in kwargs or not kwargs['access_token']:
|
|
306
|
+
try:
|
|
307
|
+
access_token = await auth.get_current_access_token()
|
|
308
|
+
if access_token:
|
|
309
|
+
kwargs['access_token'] = access_token
|
|
310
|
+
logger.debug("Using access token from auth_manager")
|
|
311
|
+
else:
|
|
312
|
+
logger.warning("No access token available from auth_manager")
|
|
313
|
+
# Add more details about why token might be missing
|
|
314
|
+
if (auth_manager.app_id == "YOUR_META_APP_ID" or not auth_manager.app_id) and not auth_manager.use_pipeboard:
|
|
315
|
+
logger.error("TOKEN VALIDATION FAILED: No valid app_id configured")
|
|
316
|
+
logger.error("Please set META_APP_ID environment variable or configure in your code")
|
|
317
|
+
elif auth_manager.use_pipeboard:
|
|
318
|
+
logger.error("TOKEN VALIDATION FAILED: Pipeboard authentication enabled but no valid token available")
|
|
319
|
+
logger.error("Complete authentication via Pipeboard service or check PIPEBOARD_API_TOKEN")
|
|
320
|
+
else:
|
|
321
|
+
logger.error("Check logs above for detailed token validation failures")
|
|
322
|
+
except Exception as e:
|
|
323
|
+
logger.error(f"Error getting access token: {str(e)}")
|
|
324
|
+
# Add stack trace for better debugging
|
|
325
|
+
import traceback
|
|
326
|
+
logger.error(f"Stack trace: {traceback.format_exc()}")
|
|
327
|
+
|
|
328
|
+
# Final validation - if we still don't have a valid token, return authentication required
|
|
329
|
+
if 'access_token' not in kwargs or not kwargs['access_token']:
|
|
330
|
+
logger.warning("No access token available, authentication needed")
|
|
331
|
+
|
|
332
|
+
# Add more specific troubleshooting information
|
|
333
|
+
auth_url = auth_manager.get_auth_url()
|
|
334
|
+
app_id = auth_manager.app_id
|
|
335
|
+
using_pipeboard = auth_manager.use_pipeboard
|
|
336
|
+
|
|
337
|
+
logger.error("TOKEN VALIDATION SUMMARY:")
|
|
338
|
+
logger.error(f"- Current app_id: '{app_id}'")
|
|
339
|
+
logger.error(f"- Environment META_APP_ID: '{os.environ.get('META_APP_ID', 'Not set')}'")
|
|
340
|
+
logger.error(f"- Pipeboard API token configured: {'Yes' if os.environ.get('PIPEBOARD_API_TOKEN') else 'No'}")
|
|
341
|
+
logger.error(f"- Using Pipeboard authentication: {'Yes' if using_pipeboard else 'No'}")
|
|
342
|
+
|
|
343
|
+
# Check for common configuration issues - but only if not using Pipeboard
|
|
344
|
+
if not using_pipeboard and (app_id == "YOUR_META_APP_ID" or not app_id):
|
|
345
|
+
logger.error("ISSUE DETECTED: No valid Meta App ID configured")
|
|
346
|
+
logger.error("ACTION REQUIRED: Set META_APP_ID environment variable with a valid App ID")
|
|
347
|
+
elif using_pipeboard:
|
|
348
|
+
logger.error("ISSUE DETECTED: Pipeboard authentication configured but no valid token available")
|
|
349
|
+
logger.error("ACTION REQUIRED: Complete authentication via Pipeboard service")
|
|
350
|
+
|
|
351
|
+
# Provide different guidance based on authentication method
|
|
352
|
+
if using_pipeboard:
|
|
353
|
+
return json.dumps({
|
|
354
|
+
"error": {
|
|
355
|
+
"message": "Pipeboard Authentication Required",
|
|
356
|
+
"details": {
|
|
357
|
+
"description": "Your Pipeboard API token is invalid or has expired",
|
|
358
|
+
"action_required": "Update your Pipeboard token",
|
|
359
|
+
"setup_url": "https://pipeboard.co/setup",
|
|
360
|
+
"token_url": "https://pipeboard.co/api-tokens",
|
|
361
|
+
"configuration_status": {
|
|
362
|
+
"app_id_configured": bool(app_id) and app_id != "YOUR_META_APP_ID",
|
|
363
|
+
"pipeboard_enabled": True,
|
|
364
|
+
},
|
|
365
|
+
"troubleshooting": "Go to https://pipeboard.co/setup to verify your account setup, then visit https://pipeboard.co/api-tokens to obtain a new API token",
|
|
366
|
+
"setup_link": "[Verify your Pipeboard account setup](https://pipeboard.co/setup)",
|
|
367
|
+
"token_link": "[Get a new Pipeboard API token](https://pipeboard.co/api-tokens)"
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}, indent=2)
|
|
371
|
+
else:
|
|
372
|
+
return json.dumps({
|
|
373
|
+
"error": {
|
|
374
|
+
"message": "Authentication Required",
|
|
375
|
+
"details": {
|
|
376
|
+
"description": "You need to authenticate with the Meta API before using this tool",
|
|
377
|
+
"action_required": "Please authenticate first",
|
|
378
|
+
"auth_url": auth_url,
|
|
379
|
+
"configuration_status": {
|
|
380
|
+
"app_id_configured": bool(app_id) and app_id != "YOUR_META_APP_ID",
|
|
381
|
+
"pipeboard_enabled": False,
|
|
382
|
+
},
|
|
383
|
+
"troubleshooting": "Check logs for TOKEN VALIDATION FAILED messages",
|
|
384
|
+
"markdown_link": f"[Click here to authenticate with Meta Ads API]({auth_url})"
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}, indent=2)
|
|
388
|
+
|
|
389
|
+
# Call the original function
|
|
390
|
+
result = await func(*args, **kwargs)
|
|
391
|
+
|
|
392
|
+
# If the result is a string (JSON), try to parse it to check for errors
|
|
393
|
+
if isinstance(result, str):
|
|
394
|
+
try:
|
|
395
|
+
result_dict = json.loads(result)
|
|
396
|
+
if "error" in result_dict:
|
|
397
|
+
logger.error(f"Error in API response: {result_dict['error']}")
|
|
398
|
+
# If this is an app ID error, log more details
|
|
399
|
+
if isinstance(result_dict.get("details", {}).get("error", {}), dict):
|
|
400
|
+
error_obj = result_dict["details"]["error"]
|
|
401
|
+
if error_obj.get("code") == 200 and "Provide valid app ID" in error_obj.get("message", ""):
|
|
402
|
+
logger.error("Meta API authentication configuration issue")
|
|
403
|
+
logger.error(f"Current app_id: {app_id}")
|
|
404
|
+
# Replace the confusing error with a more user-friendly one
|
|
405
|
+
return json.dumps({
|
|
406
|
+
"error": {
|
|
407
|
+
"message": "Meta API Configuration Issue",
|
|
408
|
+
"details": {
|
|
409
|
+
"description": "Your Meta API app is not properly configured",
|
|
410
|
+
"action_required": "Check your META_APP_ID environment variable",
|
|
411
|
+
"current_app_id": app_id,
|
|
412
|
+
"original_error": error_obj.get("message")
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}, indent=2)
|
|
416
|
+
except Exception:
|
|
417
|
+
# Not JSON or other parsing error, wrap it in a dictionary
|
|
418
|
+
return json.dumps({"data": result}, indent=2)
|
|
419
|
+
|
|
420
|
+
# If result is already a dictionary, ensure it's properly serialized
|
|
421
|
+
if isinstance(result, dict):
|
|
422
|
+
return json.dumps(result, indent=2)
|
|
423
|
+
|
|
424
|
+
return result
|
|
425
|
+
except McpToolError:
|
|
426
|
+
raise # Let FastMCP set isError: true and refund the usage credit
|
|
427
|
+
except Exception as e:
|
|
428
|
+
logger.error(f"Error in {func.__name__}: {str(e)}")
|
|
429
|
+
return json.dumps({"error": str(e)}, indent=2)
|
|
430
|
+
|
|
431
|
+
return wrapper
|