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
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FastMCP HTTP Authentication Integration for Meta Ads MCP
|
|
3
|
+
|
|
4
|
+
This module provides direct integration with FastMCP to inject authentication
|
|
5
|
+
from HTTP headers into the tool execution context.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import contextvars
|
|
10
|
+
from typing import Optional
|
|
11
|
+
from .utils import logger
|
|
12
|
+
import json
|
|
13
|
+
|
|
14
|
+
# Use context variables instead of thread-local storage for better async support
|
|
15
|
+
_auth_token: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar('auth_token', default=None)
|
|
16
|
+
_pipeboard_token: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar('pipeboard_token', default=None)
|
|
17
|
+
|
|
18
|
+
class FastMCPAuthIntegration:
|
|
19
|
+
"""Direct integration with FastMCP for HTTP authentication"""
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def set_auth_token(token: str) -> None:
|
|
23
|
+
"""Set authentication token for the current context
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
token: Access token to use for this request
|
|
27
|
+
"""
|
|
28
|
+
_auth_token.set(token)
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def get_auth_token() -> Optional[str]:
|
|
32
|
+
"""Get authentication token for the current context
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Access token if set, None otherwise
|
|
36
|
+
"""
|
|
37
|
+
return _auth_token.get(None)
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def set_pipeboard_token(token: str) -> None:
|
|
41
|
+
"""Set Pipeboard token for the current context
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
token: Pipeboard API token to use for this request
|
|
45
|
+
"""
|
|
46
|
+
_pipeboard_token.set(token)
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def get_pipeboard_token() -> Optional[str]:
|
|
50
|
+
"""Get Pipeboard token for the current context
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Pipeboard token if set, None otherwise
|
|
54
|
+
"""
|
|
55
|
+
return _pipeboard_token.get(None)
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def clear_auth_token() -> None:
|
|
59
|
+
"""Clear authentication token for the current context"""
|
|
60
|
+
_auth_token.set(None)
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def clear_pipeboard_token() -> None:
|
|
64
|
+
"""Clear Pipeboard token for the current context"""
|
|
65
|
+
_pipeboard_token.set(None)
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def extract_token_from_headers(headers: dict) -> Optional[str]:
|
|
69
|
+
"""Extract token from HTTP headers
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
headers: HTTP request headers
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Token if found, None otherwise
|
|
76
|
+
"""
|
|
77
|
+
# Check for Bearer token in Authorization header (primary method)
|
|
78
|
+
auth_header = headers.get('Authorization') or headers.get('authorization')
|
|
79
|
+
if auth_header and auth_header.lower().startswith('bearer '):
|
|
80
|
+
token = auth_header[7:].strip()
|
|
81
|
+
logger.debug("Found Bearer token in Authorization header")
|
|
82
|
+
return token
|
|
83
|
+
|
|
84
|
+
# Check for direct Meta access token
|
|
85
|
+
meta_token = headers.get('X-META-ACCESS-TOKEN') or headers.get('x-meta-access-token')
|
|
86
|
+
if meta_token:
|
|
87
|
+
return meta_token
|
|
88
|
+
|
|
89
|
+
# Check for Pipeboard token (legacy support, to be removed)
|
|
90
|
+
pipeboard_token = headers.get('X-PIPEBOARD-API-TOKEN') or headers.get('x-pipeboard-api-token')
|
|
91
|
+
if pipeboard_token:
|
|
92
|
+
logger.debug("Found Pipeboard token in legacy headers")
|
|
93
|
+
return pipeboard_token
|
|
94
|
+
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def extract_pipeboard_token_from_headers(headers: dict) -> Optional[str]:
|
|
99
|
+
"""Extract Pipeboard token from HTTP headers
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
headers: HTTP request headers
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Pipeboard token if found, None otherwise
|
|
106
|
+
"""
|
|
107
|
+
# Check for Pipeboard token in X-Pipeboard-Token header (duplication API pattern)
|
|
108
|
+
pipeboard_token = headers.get('X-Pipeboard-Token') or headers.get('x-pipeboard-token')
|
|
109
|
+
if pipeboard_token:
|
|
110
|
+
logger.debug("Found Pipeboard token in X-Pipeboard-Token header")
|
|
111
|
+
return pipeboard_token
|
|
112
|
+
|
|
113
|
+
# Check for legacy Pipeboard token header
|
|
114
|
+
legacy_token = headers.get('X-PIPEBOARD-API-TOKEN') or headers.get('x-pipeboard-api-token')
|
|
115
|
+
if legacy_token:
|
|
116
|
+
logger.debug("Found Pipeboard token in legacy X-PIPEBOARD-API-TOKEN header")
|
|
117
|
+
return legacy_token
|
|
118
|
+
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
def patch_fastmcp_server(mcp_server):
|
|
122
|
+
"""Patch FastMCP server to inject authentication from HTTP headers
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
mcp_server: FastMCP server instance to patch
|
|
126
|
+
"""
|
|
127
|
+
logger.info("Patching FastMCP server for HTTP authentication")
|
|
128
|
+
|
|
129
|
+
# Store the original run method
|
|
130
|
+
original_run = mcp_server.run
|
|
131
|
+
|
|
132
|
+
def patched_run(transport="stdio", **kwargs):
|
|
133
|
+
"""Enhanced run method that sets up HTTP auth integration"""
|
|
134
|
+
logger.debug(f"Starting FastMCP with transport: {transport}")
|
|
135
|
+
|
|
136
|
+
if transport == "streamable-http":
|
|
137
|
+
logger.debug("Setting up HTTP authentication for streamable-http transport")
|
|
138
|
+
setup_http_auth_patching()
|
|
139
|
+
|
|
140
|
+
# Call the original run method
|
|
141
|
+
return original_run(transport=transport, **kwargs)
|
|
142
|
+
|
|
143
|
+
# Replace the run method
|
|
144
|
+
mcp_server.run = patched_run
|
|
145
|
+
logger.info("FastMCP server patching complete")
|
|
146
|
+
|
|
147
|
+
def setup_http_auth_patching():
|
|
148
|
+
"""Setup HTTP authentication patching for auth system"""
|
|
149
|
+
logger.info("Setting up HTTP authentication patching")
|
|
150
|
+
|
|
151
|
+
# Import and patch the auth system
|
|
152
|
+
from . import auth
|
|
153
|
+
from . import api
|
|
154
|
+
from . import authentication
|
|
155
|
+
|
|
156
|
+
# Store the original function
|
|
157
|
+
original_get_current_access_token = auth.get_current_access_token
|
|
158
|
+
|
|
159
|
+
async def get_current_access_token_with_http_support() -> Optional[str]:
|
|
160
|
+
"""Enhanced get_current_access_token that checks HTTP context first"""
|
|
161
|
+
|
|
162
|
+
# Check for context-scoped token first
|
|
163
|
+
context_token = FastMCPAuthIntegration.get_auth_token()
|
|
164
|
+
if context_token:
|
|
165
|
+
return context_token
|
|
166
|
+
|
|
167
|
+
# Fall back to original implementation
|
|
168
|
+
return await original_get_current_access_token()
|
|
169
|
+
|
|
170
|
+
# Replace the function in all modules that imported it
|
|
171
|
+
auth.get_current_access_token = get_current_access_token_with_http_support
|
|
172
|
+
api.get_current_access_token = get_current_access_token_with_http_support
|
|
173
|
+
authentication.get_current_access_token = get_current_access_token_with_http_support
|
|
174
|
+
|
|
175
|
+
logger.info("Auth system patching complete - patched in auth, api, and authentication modules")
|
|
176
|
+
|
|
177
|
+
# Global instance for easy access
|
|
178
|
+
fastmcp_auth = FastMCPAuthIntegration()
|
|
179
|
+
|
|
180
|
+
# Forward declaration of setup_starlette_middleware
|
|
181
|
+
def setup_starlette_middleware(app):
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
def setup_fastmcp_http_auth(mcp_server):
|
|
185
|
+
"""Setup HTTP authentication integration with FastMCP
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
mcp_server: FastMCP server instance to configure
|
|
189
|
+
"""
|
|
190
|
+
logger.info("Setting up FastMCP HTTP authentication integration")
|
|
191
|
+
|
|
192
|
+
# 1. Patch FastMCP's run method to ensure our get_current_access_token patch is applied
|
|
193
|
+
# This remains crucial for the token to be picked up by tool calls.
|
|
194
|
+
patch_fastmcp_server(mcp_server) # This patches mcp_server.run
|
|
195
|
+
|
|
196
|
+
# 2. Patch the methods that provide the Starlette app instance
|
|
197
|
+
# This ensures our middleware is added to the app Uvicorn will actually serve.
|
|
198
|
+
|
|
199
|
+
app_provider_methods = []
|
|
200
|
+
if mcp_server.settings.json_response:
|
|
201
|
+
if hasattr(mcp_server, "streamable_http_app") and callable(mcp_server.streamable_http_app):
|
|
202
|
+
app_provider_methods.append("streamable_http_app")
|
|
203
|
+
else:
|
|
204
|
+
logger.warning("mcp_server.streamable_http_app not found or not callable, cannot patch for JSON responses.")
|
|
205
|
+
else: # SSE
|
|
206
|
+
if hasattr(mcp_server, "sse_app") and callable(mcp_server.sse_app):
|
|
207
|
+
app_provider_methods.append("sse_app")
|
|
208
|
+
else:
|
|
209
|
+
logger.warning("mcp_server.sse_app not found or not callable, cannot patch for SSE responses.")
|
|
210
|
+
|
|
211
|
+
if not app_provider_methods:
|
|
212
|
+
logger.error("No suitable app provider method (streamable_http_app or sse_app) found on mcp_server. Cannot add HTTP Auth middleware.")
|
|
213
|
+
# Fallback or error handling might be needed here if this is critical
|
|
214
|
+
|
|
215
|
+
for method_name in app_provider_methods:
|
|
216
|
+
original_app_provider_method = getattr(mcp_server, method_name)
|
|
217
|
+
|
|
218
|
+
def new_patched_app_provider_method(*args, **kwargs):
|
|
219
|
+
# Call the original method to get/create the Starlette app
|
|
220
|
+
app = original_app_provider_method(*args, **kwargs)
|
|
221
|
+
if app:
|
|
222
|
+
logger.debug(f"Original {method_name} returned app: {type(app)}. Adding AuthInjectionMiddleware.")
|
|
223
|
+
# Now, add our middleware to this specific app instance
|
|
224
|
+
setup_starlette_middleware(app)
|
|
225
|
+
else:
|
|
226
|
+
logger.error(f"Original {method_name} returned None or a non-app object.")
|
|
227
|
+
return app
|
|
228
|
+
|
|
229
|
+
setattr(mcp_server, method_name, new_patched_app_provider_method)
|
|
230
|
+
logger.debug(f"Patched mcp_server.{method_name} to inject AuthInjectionMiddleware.")
|
|
231
|
+
|
|
232
|
+
# The old setup_request_middleware call is no longer needed here,
|
|
233
|
+
# as middleware addition is now handled by patching the app provider methods.
|
|
234
|
+
# try:
|
|
235
|
+
# setup_request_middleware(mcp_server)
|
|
236
|
+
# except Exception as e:
|
|
237
|
+
# logger.warning(f"Could not setup request middleware: {e}")
|
|
238
|
+
|
|
239
|
+
logger.info("FastMCP HTTP authentication integration setup attempt complete.")
|
|
240
|
+
|
|
241
|
+
# Remove the old setup_request_middleware function as its logic is integrated above
|
|
242
|
+
# def setup_request_middleware(mcp_server): ... (delete this function)
|
|
243
|
+
|
|
244
|
+
# --- AuthInjectionMiddleware definition ---
|
|
245
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
246
|
+
from starlette.requests import Request
|
|
247
|
+
import json # Ensure json is imported if not already at the top
|
|
248
|
+
|
|
249
|
+
class AuthInjectionMiddleware(BaseHTTPMiddleware):
|
|
250
|
+
async def dispatch(self, request: Request, call_next):
|
|
251
|
+
logger.debug(f"HTTP Auth Middleware: Processing request to {request.url.path}")
|
|
252
|
+
logger.debug(f"HTTP Auth Middleware: Request headers: {list(request.headers.keys())}")
|
|
253
|
+
|
|
254
|
+
# Extract both types of tokens for dual-header authentication
|
|
255
|
+
auth_token = FastMCPAuthIntegration.extract_token_from_headers(dict(request.headers))
|
|
256
|
+
pipeboard_token = FastMCPAuthIntegration.extract_pipeboard_token_from_headers(dict(request.headers))
|
|
257
|
+
|
|
258
|
+
if auth_token:
|
|
259
|
+
logger.debug(f"HTTP Auth Middleware: Extracted auth token: {auth_token[:10]}...")
|
|
260
|
+
logger.debug("Injecting auth token into request context")
|
|
261
|
+
FastMCPAuthIntegration.set_auth_token(auth_token)
|
|
262
|
+
|
|
263
|
+
if pipeboard_token:
|
|
264
|
+
logger.debug(f"HTTP Auth Middleware: Extracted Pipeboard token: {pipeboard_token[:10]}...")
|
|
265
|
+
logger.debug("Injecting Pipeboard token into request context")
|
|
266
|
+
FastMCPAuthIntegration.set_pipeboard_token(pipeboard_token)
|
|
267
|
+
|
|
268
|
+
if not auth_token and not pipeboard_token:
|
|
269
|
+
logger.warning("HTTP Auth Middleware: No authentication tokens found in headers")
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
response = await call_next(request)
|
|
273
|
+
return response
|
|
274
|
+
finally:
|
|
275
|
+
# Clear tokens that were set for this request
|
|
276
|
+
if auth_token:
|
|
277
|
+
FastMCPAuthIntegration.clear_auth_token()
|
|
278
|
+
if pipeboard_token:
|
|
279
|
+
FastMCPAuthIntegration.clear_pipeboard_token()
|
|
280
|
+
|
|
281
|
+
def setup_starlette_middleware(app):
|
|
282
|
+
"""Add AuthInjectionMiddleware to the Starlette app if not already present.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
app: Starlette app instance
|
|
286
|
+
"""
|
|
287
|
+
if not app:
|
|
288
|
+
logger.error("Cannot setup Starlette middleware, app is None.")
|
|
289
|
+
return
|
|
290
|
+
|
|
291
|
+
# Check if our specific middleware class is already in the stack
|
|
292
|
+
already_added = False
|
|
293
|
+
# Starlette's app.middleware is a list of Middleware objects.
|
|
294
|
+
# app.user_middleware contains middleware added by app.add_middleware()
|
|
295
|
+
for middleware_item in app.user_middleware:
|
|
296
|
+
if middleware_item.cls == AuthInjectionMiddleware:
|
|
297
|
+
already_added = True
|
|
298
|
+
break
|
|
299
|
+
|
|
300
|
+
if not already_added:
|
|
301
|
+
try:
|
|
302
|
+
app.add_middleware(AuthInjectionMiddleware)
|
|
303
|
+
logger.info("AuthInjectionMiddleware added to Starlette app successfully.")
|
|
304
|
+
except Exception as e:
|
|
305
|
+
logger.error(f"Failed to add AuthInjectionMiddleware to Starlette app: {e}", exc_info=True)
|
|
306
|
+
else:
|
|
307
|
+
logger.debug("AuthInjectionMiddleware already present in Starlette app's middleware stack.")
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Insights and Reporting functionality for Meta Ads API."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Optional, Union, Dict, List
|
|
5
|
+
from .api import meta_api_tool, make_api_request
|
|
6
|
+
from .utils import download_image, try_multiple_download_methods, ad_creative_images, create_resource_from_image
|
|
7
|
+
from .server import mcp_server
|
|
8
|
+
import base64
|
|
9
|
+
import datetime
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Prefixes of action_type values that are always redundant duplicates of other
|
|
13
|
+
# action types already present in the response. For every canonical event
|
|
14
|
+
# (e.g. "purchase"), the Meta API returns 5-8 variants that carry the exact
|
|
15
|
+
# same numeric value:
|
|
16
|
+
# omni_purchase, onsite_web_purchase, onsite_web_app_purchase,
|
|
17
|
+
# web_in_store_purchase, web_app_in_store_purchase,
|
|
18
|
+
# offsite_conversion.fb_pixel_purchase ...
|
|
19
|
+
# Removing these cuts each insight row from ~4 KB to ~1 KB without any
|
|
20
|
+
# information loss.
|
|
21
|
+
_REDUNDANT_ACTION_PREFIXES = (
|
|
22
|
+
"omni_", # omnichannel roll-up (== onsite_web_app_*)
|
|
23
|
+
"onsite_web_app_", # web+app combined (== onsite_web_*)
|
|
24
|
+
"onsite_web_", # web-only subset (== canonical + onsite)
|
|
25
|
+
"onsite_app_", # app-only subset (== onsite_conversion.*)
|
|
26
|
+
"web_app_in_store_", # web+app in-store (== web_in_store_*)
|
|
27
|
+
"offsite_conversion.fb_pixel_", # pixel attribution (== canonical type)
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _strip_redundant_actions(row: dict) -> dict:
|
|
32
|
+
"""Remove redundant action-type entries from a single insight row."""
|
|
33
|
+
for key in ("actions", "action_values", "cost_per_action_type"):
|
|
34
|
+
items = row.get(key)
|
|
35
|
+
if not isinstance(items, list):
|
|
36
|
+
continue
|
|
37
|
+
row[key] = [
|
|
38
|
+
item for item in items
|
|
39
|
+
if not any(
|
|
40
|
+
item.get("action_type", "").startswith(prefix)
|
|
41
|
+
for prefix in _REDUNDANT_ACTION_PREFIXES
|
|
42
|
+
)
|
|
43
|
+
]
|
|
44
|
+
return row
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@mcp_server.tool()
|
|
48
|
+
@meta_api_tool
|
|
49
|
+
async def get_insights(object_id: str = "", access_token: Optional[str] = None,
|
|
50
|
+
time_range: Union[str, Dict[str, str]] = "maximum", breakdown: str = "",
|
|
51
|
+
level: str = "ad", limit: int = 25, after: str = "",
|
|
52
|
+
action_attribution_windows: Optional[List[str]] = None,
|
|
53
|
+
compact: bool = False,
|
|
54
|
+
account_id: str = "", campaign_id: str = "",
|
|
55
|
+
adset_id: str = "", ad_id: str = "") -> str:
|
|
56
|
+
"""
|
|
57
|
+
Get performance insights for a campaign, ad set, ad or account.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
object_id: ID of the campaign, ad set, ad or account. You can also use the alias parameters below.
|
|
61
|
+
account_id: Alias for object_id when querying account-level insights
|
|
62
|
+
campaign_id: Alias for object_id when querying campaign-level insights
|
|
63
|
+
adset_id: Alias for object_id when querying ad-set-level insights
|
|
64
|
+
ad_id: Alias for object_id when querying ad-level insights
|
|
65
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
66
|
+
time_range: Either a preset time range string or a dictionary with "since" and "until" dates in YYYY-MM-DD format
|
|
67
|
+
Preset options: today, yesterday, this_month, last_month, this_quarter, maximum, data_maximum,
|
|
68
|
+
last_3d, last_7d, last_14d, last_28d, last_30d, last_90d, last_week_mon_sun,
|
|
69
|
+
last_week_sun_sat, last_quarter, last_year, this_week_mon_today, this_week_sun_today, this_year
|
|
70
|
+
Dictionary example: {"since":"2023-01-01","until":"2023-01-31"}
|
|
71
|
+
breakdown: Optional breakdown dimension. Valid values include:
|
|
72
|
+
Demographic: age, gender, country, region, dma
|
|
73
|
+
Platform/Device: device_platform, platform_position, publisher_platform, impression_device
|
|
74
|
+
Creative Assets: ad_format_asset, body_asset, call_to_action_asset, description_asset,
|
|
75
|
+
image_asset, link_url_asset, title_asset, video_asset, media_asset_url,
|
|
76
|
+
media_creator, media_destination_url, media_format, media_origin_url,
|
|
77
|
+
media_text_content, media_type, creative_relaxation_asset_type,
|
|
78
|
+
flexible_format_asset_type, gen_ai_asset_type
|
|
79
|
+
Campaign/Ad Attributes: breakdown_ad_objective, breakdown_reporting_ad_id, app_id, product_id
|
|
80
|
+
Conversion Tracking: coarse_conversion_value, conversion_destination, standard_event_content_type,
|
|
81
|
+
signal_source_bucket, is_conversion_id_modeled, fidelity_type, redownload
|
|
82
|
+
Time-based: hourly_stats_aggregated_by_advertiser_time_zone,
|
|
83
|
+
hourly_stats_aggregated_by_audience_time_zone, frequency_value
|
|
84
|
+
Extensions/Landing: ad_extension_domain, ad_extension_url, landing_destination,
|
|
85
|
+
mdsa_landing_destination
|
|
86
|
+
Attribution: sot_attribution_model_type, sot_attribution_window, sot_channel,
|
|
87
|
+
sot_event_type, sot_source
|
|
88
|
+
Mobile/SKAN: skan_campaign_id, skan_conversion_id, skan_version, postback_sequence_index
|
|
89
|
+
CRM/Business: crm_advertiser_l12_territory_ids, crm_advertiser_subvertical_id,
|
|
90
|
+
crm_advertiser_vertical_id, crm_ult_advertiser_id, user_persona_id, user_persona_name
|
|
91
|
+
Advanced: hsid, is_auto_advance, is_rendered_as_delayed_skip_ad, mmm, place_page_id,
|
|
92
|
+
marketing_messages_btn_name, impression_view_time_advertiser_hour_v2, comscore_market,
|
|
93
|
+
comscore_market_code
|
|
94
|
+
level: Level of aggregation (ad, adset, campaign, account)
|
|
95
|
+
limit: Maximum number of results to return per page (default: 25, Meta API allows much higher values)
|
|
96
|
+
after: Pagination cursor to get the next set of results. Use the 'after' cursor from previous response's paging.next field.
|
|
97
|
+
action_attribution_windows: Optional list of attribution windows (e.g., ["1d_click", "7d_click", "1d_view"]).
|
|
98
|
+
When specified, actions include additional fields for each window. The 'value' field always shows 7d_click.
|
|
99
|
+
compact: When True, strips redundant action-type duplicates from the response
|
|
100
|
+
(omni_*, onsite_web_*, offsite_conversion.fb_pixel_*, etc.) to reduce
|
|
101
|
+
payload size by ~60%. The canonical action types (purchase, add_to_cart,
|
|
102
|
+
view_content, etc.) are always preserved. Default: False.
|
|
103
|
+
|
|
104
|
+
Note on response size: This tool always returns a fixed set of fields (impressions, clicks,
|
|
105
|
+
spend, cpc, cpm, ctr, reach, actions, action_values, etc.) and cannot filter to a subset.
|
|
106
|
+
For large result sets (50+ rows), the actions/action_values arrays can make responses very
|
|
107
|
+
large (1-2MB+). If you only need specific metrics like spend or impressions, consider using
|
|
108
|
+
bulk_get_insights with compact=true and the fields parameter:
|
|
109
|
+
bulk_get_insights(level="ad", account_ids=[...], compact=true, fields=["spend", "impressions"])
|
|
110
|
+
bulk_get_insights supports level="ad", "adset", "campaign", and "account".
|
|
111
|
+
"""
|
|
112
|
+
# Accept common aliases for object_id (LLMs frequently use these instead)
|
|
113
|
+
if not object_id:
|
|
114
|
+
object_id = account_id or campaign_id or adset_id or ad_id
|
|
115
|
+
|
|
116
|
+
if not object_id:
|
|
117
|
+
return json.dumps({"error": "No object ID provided. Use object_id, account_id, campaign_id, adset_id, or ad_id."}, indent=2)
|
|
118
|
+
|
|
119
|
+
endpoint = f"{object_id}/insights"
|
|
120
|
+
params = {
|
|
121
|
+
"fields": "account_id,account_name,campaign_id,campaign_name,adset_id,adset_name,ad_id,ad_name,impressions,clicks,spend,cpc,cpm,ctr,reach,frequency,actions,action_values,conversions,unique_clicks,cost_per_action_type",
|
|
122
|
+
"level": level,
|
|
123
|
+
"limit": limit
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# Handle time range based on type
|
|
127
|
+
if isinstance(time_range, dict):
|
|
128
|
+
# Use custom date range with since/until parameters
|
|
129
|
+
if "since" in time_range and "until" in time_range:
|
|
130
|
+
params["time_range"] = json.dumps(time_range)
|
|
131
|
+
else:
|
|
132
|
+
return json.dumps({"error": "Custom time_range must contain both 'since' and 'until' keys in YYYY-MM-DD format"}, indent=2)
|
|
133
|
+
else:
|
|
134
|
+
# Use preset date range
|
|
135
|
+
params["date_preset"] = time_range
|
|
136
|
+
|
|
137
|
+
if breakdown:
|
|
138
|
+
params["breakdowns"] = breakdown
|
|
139
|
+
|
|
140
|
+
if after:
|
|
141
|
+
params["after"] = after
|
|
142
|
+
|
|
143
|
+
if action_attribution_windows:
|
|
144
|
+
# Meta API expects single-quote format: ['1d_click','7d_click']
|
|
145
|
+
params["action_attribution_windows"] = "[" + ",".join(f"'{w}'" for w in action_attribution_windows) + "]"
|
|
146
|
+
|
|
147
|
+
data = await make_api_request(endpoint, access_token, params)
|
|
148
|
+
|
|
149
|
+
# In compact mode, strip redundant action-type duplicates to reduce response size.
|
|
150
|
+
if compact and isinstance(data, dict):
|
|
151
|
+
for row in data.get("data", []):
|
|
152
|
+
if isinstance(row, dict):
|
|
153
|
+
_strip_redundant_actions(row)
|
|
154
|
+
|
|
155
|
+
return json.dumps(data, indent=2)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
|