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/mcc.py
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""Meta Business Manager (MCC) functionality for Meta Ads API.
|
|
2
|
+
|
|
3
|
+
Provides tools for managing multi-client accounts via Meta Business Manager,
|
|
4
|
+
which is the Meta equivalent of Google's MCC (My Client Center).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from typing import Optional
|
|
9
|
+
from .api import meta_api_tool, make_api_request
|
|
10
|
+
from .server import mcp_server
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Fields requested for ad accounts to keep responses consistent with accounts.py
|
|
14
|
+
_AD_ACCOUNT_FIELDS = (
|
|
15
|
+
"id,name,account_id,account_status,amount_spent,balance,currency,"
|
|
16
|
+
"age,business_city,business_country_code,timezone_name"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Fields requested for Business Manager objects
|
|
20
|
+
_BUSINESS_FIELDS = (
|
|
21
|
+
"id,name,timezone_id,business_type,verification_status,profile_picture_uri"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _normalize_account_monetary_fields(account: dict) -> dict:
|
|
26
|
+
"""Convert amount_spent and balance from cents to currency units."""
|
|
27
|
+
_ZERO_DECIMAL = {
|
|
28
|
+
"BIF", "CLP", "DJF", "GNF", "JPY", "KMF", "KRW", "MGA",
|
|
29
|
+
"PYG", "RWF", "UGX", "VND", "VUV", "XAF", "XOF", "XPF",
|
|
30
|
+
}
|
|
31
|
+
currency = account.get("currency", "USD")
|
|
32
|
+
for field in ("amount_spent", "balance"):
|
|
33
|
+
if field in account:
|
|
34
|
+
try:
|
|
35
|
+
amount_int = int(account[field])
|
|
36
|
+
account[field] = (
|
|
37
|
+
str(amount_int)
|
|
38
|
+
if currency.upper() in _ZERO_DECIMAL
|
|
39
|
+
else f"{amount_int / 100:.2f}"
|
|
40
|
+
)
|
|
41
|
+
except (TypeError, ValueError):
|
|
42
|
+
pass
|
|
43
|
+
return account
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@mcp_server.tool()
|
|
47
|
+
@meta_api_tool
|
|
48
|
+
async def get_businesses(
|
|
49
|
+
access_token: Optional[str] = None,
|
|
50
|
+
limit: int = 100,
|
|
51
|
+
) -> str:
|
|
52
|
+
"""Get all Business Manager accounts (MCCs) accessible by the current user.
|
|
53
|
+
|
|
54
|
+
Returns a list of Business Manager organizations the authenticated user
|
|
55
|
+
belongs to. Each entry includes the business ID and name, which can be
|
|
56
|
+
used with other MCC tools to drill into client accounts.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
access_token: Meta API access token (optional -- uses cached token).
|
|
60
|
+
limit: Maximum number of businesses to return (default: 100).
|
|
61
|
+
"""
|
|
62
|
+
data = await make_api_request(
|
|
63
|
+
"me/businesses",
|
|
64
|
+
access_token,
|
|
65
|
+
{"fields": _BUSINESS_FIELDS, "limit": limit},
|
|
66
|
+
)
|
|
67
|
+
return json.dumps(data, indent=2)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@mcp_server.tool()
|
|
71
|
+
@meta_api_tool
|
|
72
|
+
async def get_business_ad_accounts(
|
|
73
|
+
business_id: str,
|
|
74
|
+
access_token: Optional[str] = None,
|
|
75
|
+
include_owned: bool = True,
|
|
76
|
+
include_client: bool = True,
|
|
77
|
+
limit: int = 200,
|
|
78
|
+
) -> str:
|
|
79
|
+
"""Get all ad accounts under a Business Manager (owned and/or client accounts).
|
|
80
|
+
|
|
81
|
+
Fetches ad accounts from a Meta Business Manager. Owned accounts are those
|
|
82
|
+
the business directly controls; client accounts are managed on behalf of
|
|
83
|
+
external advertisers.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
business_id: Meta Business Manager ID.
|
|
87
|
+
access_token: Meta API access token (optional -- uses cached token).
|
|
88
|
+
include_owned: Include ad accounts owned by this business (default: True).
|
|
89
|
+
include_client: Include client ad accounts managed by this business (default: True).
|
|
90
|
+
limit: Maximum number of accounts per category (default: 200).
|
|
91
|
+
"""
|
|
92
|
+
if not business_id:
|
|
93
|
+
return json.dumps({"error": "business_id is required"}, indent=2)
|
|
94
|
+
|
|
95
|
+
params = {"fields": _AD_ACCOUNT_FIELDS, "limit": limit}
|
|
96
|
+
owned: list = []
|
|
97
|
+
clients: list = []
|
|
98
|
+
errors: list = []
|
|
99
|
+
|
|
100
|
+
if include_owned:
|
|
101
|
+
result = await make_api_request(
|
|
102
|
+
f"{business_id}/owned_ad_accounts", access_token, params
|
|
103
|
+
)
|
|
104
|
+
if "error" in result:
|
|
105
|
+
errors.append({"source": "owned_ad_accounts", "error": result["error"]})
|
|
106
|
+
else:
|
|
107
|
+
owned = [_normalize_account_monetary_fields(a) for a in result.get("data", [])]
|
|
108
|
+
|
|
109
|
+
if include_client:
|
|
110
|
+
result = await make_api_request(
|
|
111
|
+
f"{business_id}/client_ad_accounts", access_token, params
|
|
112
|
+
)
|
|
113
|
+
if "error" in result:
|
|
114
|
+
errors.append({"source": "client_ad_accounts", "error": result["error"]})
|
|
115
|
+
else:
|
|
116
|
+
clients = [_normalize_account_monetary_fields(a) for a in result.get("data", [])]
|
|
117
|
+
|
|
118
|
+
response: dict = {
|
|
119
|
+
"business_id": business_id,
|
|
120
|
+
"owned_accounts": owned,
|
|
121
|
+
"owned_count": len(owned),
|
|
122
|
+
"client_accounts": clients,
|
|
123
|
+
"client_count": len(clients),
|
|
124
|
+
"total_count": len(owned) + len(clients),
|
|
125
|
+
}
|
|
126
|
+
if errors:
|
|
127
|
+
response["errors"] = errors
|
|
128
|
+
|
|
129
|
+
return json.dumps(response, indent=2)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@mcp_server.tool()
|
|
133
|
+
@meta_api_tool
|
|
134
|
+
async def find_client_account(
|
|
135
|
+
business_id: str,
|
|
136
|
+
query: str,
|
|
137
|
+
access_token: Optional[str] = None,
|
|
138
|
+
search_owned: bool = True,
|
|
139
|
+
search_client: bool = True,
|
|
140
|
+
limit: int = 200,
|
|
141
|
+
) -> str:
|
|
142
|
+
"""Find a client's ad account within a Business Manager by name or account ID.
|
|
143
|
+
|
|
144
|
+
Searches through all ad accounts under the specified Business Manager and
|
|
145
|
+
returns those whose name contains the query string (case-insensitive) or
|
|
146
|
+
whose account ID / act_ID matches exactly.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
business_id: Meta Business Manager ID to search within.
|
|
150
|
+
query: Account name substring (case-insensitive) or exact account ID
|
|
151
|
+
(with or without the "act_" prefix) to match.
|
|
152
|
+
access_token: Meta API access token (optional -- uses cached token).
|
|
153
|
+
search_owned: Search owned ad accounts (default: True).
|
|
154
|
+
search_client: Search client ad accounts (default: True).
|
|
155
|
+
limit: Maximum number of accounts to fetch before filtering (default: 200).
|
|
156
|
+
"""
|
|
157
|
+
if not business_id:
|
|
158
|
+
return json.dumps({"error": "business_id is required"}, indent=2)
|
|
159
|
+
if not query:
|
|
160
|
+
return json.dumps({"error": "query is required"}, indent=2)
|
|
161
|
+
|
|
162
|
+
params = {"fields": _AD_ACCOUNT_FIELDS, "limit": limit}
|
|
163
|
+
all_accounts: list = []
|
|
164
|
+
errors: list = []
|
|
165
|
+
|
|
166
|
+
if search_owned:
|
|
167
|
+
result = await make_api_request(
|
|
168
|
+
f"{business_id}/owned_ad_accounts", access_token, params
|
|
169
|
+
)
|
|
170
|
+
if "error" in result:
|
|
171
|
+
errors.append({"source": "owned_ad_accounts", "error": result["error"]})
|
|
172
|
+
else:
|
|
173
|
+
for acc in result.get("data", []):
|
|
174
|
+
acc["_account_type"] = "owned"
|
|
175
|
+
all_accounts.append(_normalize_account_monetary_fields(acc))
|
|
176
|
+
|
|
177
|
+
if search_client:
|
|
178
|
+
result = await make_api_request(
|
|
179
|
+
f"{business_id}/client_ad_accounts", access_token, params
|
|
180
|
+
)
|
|
181
|
+
if "error" in result:
|
|
182
|
+
errors.append({"source": "client_ad_accounts", "error": result["error"]})
|
|
183
|
+
else:
|
|
184
|
+
for acc in result.get("data", []):
|
|
185
|
+
acc["_account_type"] = "client"
|
|
186
|
+
all_accounts.append(_normalize_account_monetary_fields(acc))
|
|
187
|
+
|
|
188
|
+
# Normalize query for matching
|
|
189
|
+
q_lower = query.lower().strip()
|
|
190
|
+
q_id = q_lower.lstrip("act_") # strip act_ prefix for numeric ID comparison
|
|
191
|
+
|
|
192
|
+
matches = [
|
|
193
|
+
acc for acc in all_accounts
|
|
194
|
+
if q_lower in acc.get("name", "").lower()
|
|
195
|
+
or acc.get("account_id", "") == q_id
|
|
196
|
+
or acc.get("id", "").lstrip("act_") == q_id
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
response: dict = {
|
|
200
|
+
"business_id": business_id,
|
|
201
|
+
"query": query,
|
|
202
|
+
"matches": matches,
|
|
203
|
+
"match_count": len(matches),
|
|
204
|
+
"searched_total": len(all_accounts),
|
|
205
|
+
}
|
|
206
|
+
if errors:
|
|
207
|
+
response["errors"] = errors
|
|
208
|
+
|
|
209
|
+
return json.dumps(response, indent=2)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@mcp_server.tool()
|
|
213
|
+
@meta_api_tool
|
|
214
|
+
async def get_business_details(
|
|
215
|
+
business_id: str,
|
|
216
|
+
access_token: Optional[str] = None,
|
|
217
|
+
) -> str:
|
|
218
|
+
"""Get detailed information about a specific Business Manager.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
business_id: Meta Business Manager ID.
|
|
222
|
+
access_token: Meta API access token (optional -- uses cached token).
|
|
223
|
+
"""
|
|
224
|
+
if not business_id:
|
|
225
|
+
return json.dumps({"error": "business_id is required"}, indent=2)
|
|
226
|
+
|
|
227
|
+
data = await make_api_request(
|
|
228
|
+
business_id,
|
|
229
|
+
access_token,
|
|
230
|
+
{"fields": f"{_BUSINESS_FIELDS},owned_ad_accounts.limit(1){{id}},client_ad_accounts.limit(1){{id}}"},
|
|
231
|
+
)
|
|
232
|
+
return json.dumps(data, indent=2)
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
"""OpenAI MCP Deep Research tools for Meta Ads API.
|
|
2
|
+
|
|
3
|
+
This module implements the required 'search' and 'fetch' tools for OpenAI's
|
|
4
|
+
ChatGPT Deep Research feature, providing access to Meta Ads data in the format
|
|
5
|
+
expected by ChatGPT.
|
|
6
|
+
|
|
7
|
+
The tools expose Meta Ads data (accounts, campaigns, ads, etc.) as searchable
|
|
8
|
+
and fetchable records for ChatGPT Deep Research analysis.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
from typing import List, Dict, Any, Optional
|
|
14
|
+
from .api import meta_api_tool, make_api_request, ensure_act_prefix
|
|
15
|
+
from .server import mcp_server
|
|
16
|
+
from .utils import logger
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MetaAdsDataManager:
|
|
20
|
+
"""Manages Meta Ads data for OpenAI MCP search and fetch operations"""
|
|
21
|
+
|
|
22
|
+
def __init__(self):
|
|
23
|
+
self._cache = {}
|
|
24
|
+
logger.debug("MetaAdsDataManager initialized")
|
|
25
|
+
|
|
26
|
+
async def _get_ad_accounts(self, access_token: str, limit: int = 200) -> List[Dict[str, Any]]:
|
|
27
|
+
"""Get ad accounts data"""
|
|
28
|
+
try:
|
|
29
|
+
endpoint = "me/adaccounts"
|
|
30
|
+
params = {
|
|
31
|
+
"fields": "id,name,account_id,account_status,amount_spent,balance,currency,business_city,business_country_code",
|
|
32
|
+
"limit": limit
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
data = await make_api_request(endpoint, access_token, params)
|
|
36
|
+
|
|
37
|
+
if "data" in data:
|
|
38
|
+
return data["data"]
|
|
39
|
+
return []
|
|
40
|
+
except Exception as e:
|
|
41
|
+
logger.error(f"Error fetching ad accounts: {e}")
|
|
42
|
+
return []
|
|
43
|
+
|
|
44
|
+
async def _get_campaigns(self, access_token: str, account_id: str, limit: int = 25) -> List[Dict[str, Any]]:
|
|
45
|
+
"""Get campaigns data for an account"""
|
|
46
|
+
try:
|
|
47
|
+
endpoint = f"{account_id}/campaigns"
|
|
48
|
+
params = {
|
|
49
|
+
"fields": "id,name,status,objective,daily_budget,lifetime_budget,start_time,stop_time,created_time,updated_time",
|
|
50
|
+
"limit": limit
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
data = await make_api_request(endpoint, access_token, params)
|
|
54
|
+
|
|
55
|
+
if "data" in data:
|
|
56
|
+
return data["data"]
|
|
57
|
+
return []
|
|
58
|
+
except Exception as e:
|
|
59
|
+
logger.error(f"Error fetching campaigns for {account_id}: {e}")
|
|
60
|
+
return []
|
|
61
|
+
|
|
62
|
+
async def _get_ads(self, access_token: str, account_id: str, limit: int = 25) -> List[Dict[str, Any]]:
|
|
63
|
+
"""Get ads data for an account"""
|
|
64
|
+
try:
|
|
65
|
+
endpoint = f"{account_id}/ads"
|
|
66
|
+
params = {
|
|
67
|
+
"fields": "id,name,status,creative,targeting,bid_amount,created_time,updated_time",
|
|
68
|
+
"limit": limit
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
data = await make_api_request(endpoint, access_token, params)
|
|
72
|
+
|
|
73
|
+
if "data" in data:
|
|
74
|
+
return data["data"]
|
|
75
|
+
return []
|
|
76
|
+
except Exception as e:
|
|
77
|
+
logger.error(f"Error fetching ads for {account_id}: {e}")
|
|
78
|
+
return []
|
|
79
|
+
|
|
80
|
+
async def _get_pages_for_account(self, access_token: str, account_id: str) -> List[Dict[str, Any]]:
|
|
81
|
+
"""Get pages associated with an account"""
|
|
82
|
+
try:
|
|
83
|
+
# Import the page discovery function from ads module
|
|
84
|
+
from .ads import _discover_pages_for_account
|
|
85
|
+
|
|
86
|
+
account_id = ensure_act_prefix(account_id)
|
|
87
|
+
|
|
88
|
+
page_discovery_result = await _discover_pages_for_account(account_id, access_token)
|
|
89
|
+
|
|
90
|
+
if not page_discovery_result.get("success"):
|
|
91
|
+
return []
|
|
92
|
+
|
|
93
|
+
# Return page data in a consistent format
|
|
94
|
+
return [{
|
|
95
|
+
"id": page_discovery_result["page_id"],
|
|
96
|
+
"name": page_discovery_result.get("page_name", "Unknown"),
|
|
97
|
+
"source": page_discovery_result.get("source", "unknown"),
|
|
98
|
+
"account_id": account_id
|
|
99
|
+
}]
|
|
100
|
+
except Exception as e:
|
|
101
|
+
logger.error(f"Error fetching pages for {account_id}: {e}")
|
|
102
|
+
return []
|
|
103
|
+
|
|
104
|
+
async def _get_businesses(self, access_token: str, user_id: str = "me", limit: int = 25) -> List[Dict[str, Any]]:
|
|
105
|
+
"""Get businesses accessible by the current user"""
|
|
106
|
+
try:
|
|
107
|
+
endpoint = f"{user_id}/businesses"
|
|
108
|
+
params = {
|
|
109
|
+
"fields": "id,name,created_time,verification_status",
|
|
110
|
+
"limit": limit
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
data = await make_api_request(endpoint, access_token, params)
|
|
114
|
+
|
|
115
|
+
if "data" in data:
|
|
116
|
+
return data["data"]
|
|
117
|
+
return []
|
|
118
|
+
except Exception as e:
|
|
119
|
+
logger.error(f"Error fetching businesses: {e}")
|
|
120
|
+
return []
|
|
121
|
+
|
|
122
|
+
async def search_records(self, query: str, access_token: str) -> List[str]:
|
|
123
|
+
"""Search Meta Ads data and return matching record IDs
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
query: Search query string
|
|
127
|
+
access_token: Meta API access token
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
List of record IDs that match the query
|
|
131
|
+
"""
|
|
132
|
+
logger.info(f"Searching Meta Ads data with query: {query}")
|
|
133
|
+
|
|
134
|
+
# Normalize query for matching
|
|
135
|
+
query_lower = query.lower()
|
|
136
|
+
query_terms = re.findall(r'\w+', query_lower)
|
|
137
|
+
|
|
138
|
+
matching_ids = []
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
# Search ad accounts
|
|
142
|
+
accounts = await self._get_ad_accounts(access_token, limit=200)
|
|
143
|
+
for account in accounts:
|
|
144
|
+
account_text = f"{account.get('name', '')} {account.get('id', '')} {account.get('account_status', '')} {account.get('business_city', '')} {account.get('business_country_code', '')}".lower()
|
|
145
|
+
|
|
146
|
+
if any(term in account_text for term in query_terms):
|
|
147
|
+
record_id = f"account:{account['id']}"
|
|
148
|
+
matching_ids.append(record_id)
|
|
149
|
+
|
|
150
|
+
# Cache the account data
|
|
151
|
+
self._cache[record_id] = {
|
|
152
|
+
"id": record_id,
|
|
153
|
+
"type": "account",
|
|
154
|
+
"title": f"Ad Account: {account.get('name', 'Unnamed Account')}",
|
|
155
|
+
"text": f"Meta Ads Account {account.get('name', 'Unnamed')} (ID: {account.get('id', 'N/A')}) - Status: {account.get('account_status', 'Unknown')}, Currency: {account.get('currency', 'Unknown')}, Spent: ${account.get('amount_spent', 0)}, Balance: ${account.get('balance', 0)}",
|
|
156
|
+
"metadata": {
|
|
157
|
+
"account_id": account.get('id'),
|
|
158
|
+
"account_name": account.get('name'),
|
|
159
|
+
"status": account.get('account_status'),
|
|
160
|
+
"currency": account.get('currency'),
|
|
161
|
+
"business_location": f"{account.get('business_city', '')}, {account.get('business_country_code', '')}".strip(', '),
|
|
162
|
+
"data_type": "meta_ads_account"
|
|
163
|
+
},
|
|
164
|
+
"raw_data": account
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
# Also search campaigns for this account if it matches
|
|
168
|
+
campaigns = await self._get_campaigns(access_token, account['id'], limit=10)
|
|
169
|
+
for campaign in campaigns:
|
|
170
|
+
campaign_text = f"{campaign.get('name', '')} {campaign.get('objective', '')} {campaign.get('status', '')}".lower()
|
|
171
|
+
|
|
172
|
+
if any(term in campaign_text for term in query_terms):
|
|
173
|
+
campaign_record_id = f"campaign:{campaign['id']}"
|
|
174
|
+
matching_ids.append(campaign_record_id)
|
|
175
|
+
|
|
176
|
+
# Cache the campaign data
|
|
177
|
+
self._cache[campaign_record_id] = {
|
|
178
|
+
"id": campaign_record_id,
|
|
179
|
+
"type": "campaign",
|
|
180
|
+
"title": f"Campaign: {campaign.get('name', 'Unnamed Campaign')}",
|
|
181
|
+
"text": f"Meta Ads Campaign {campaign.get('name', 'Unnamed')} (ID: {campaign.get('id', 'N/A')}) - Objective: {campaign.get('objective', 'Unknown')}, Status: {campaign.get('status', 'Unknown')}, Daily Budget: ${campaign.get('daily_budget', 'Not set')}, Account: {account.get('name', 'Unknown')}",
|
|
182
|
+
"metadata": {
|
|
183
|
+
"campaign_id": campaign.get('id'),
|
|
184
|
+
"campaign_name": campaign.get('name'),
|
|
185
|
+
"objective": campaign.get('objective'),
|
|
186
|
+
"status": campaign.get('status'),
|
|
187
|
+
"account_id": account.get('id'),
|
|
188
|
+
"account_name": account.get('name'),
|
|
189
|
+
"data_type": "meta_ads_campaign"
|
|
190
|
+
},
|
|
191
|
+
"raw_data": campaign
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
# If query specifically mentions "ads" or "ad", also search individual ads
|
|
195
|
+
if any(term in ['ad', 'ads', 'advertisement', 'creative'] for term in query_terms):
|
|
196
|
+
for account in accounts[:3]: # Limit to first 3 accounts for performance
|
|
197
|
+
ads = await self._get_ads(access_token, account['id'], limit=10)
|
|
198
|
+
for ad in ads:
|
|
199
|
+
ad_text = f"{ad.get('name', '')} {ad.get('status', '')}".lower()
|
|
200
|
+
|
|
201
|
+
if any(term in ad_text for term in query_terms):
|
|
202
|
+
ad_record_id = f"ad:{ad['id']}"
|
|
203
|
+
matching_ids.append(ad_record_id)
|
|
204
|
+
|
|
205
|
+
# Cache the ad data
|
|
206
|
+
self._cache[ad_record_id] = {
|
|
207
|
+
"id": ad_record_id,
|
|
208
|
+
"type": "ad",
|
|
209
|
+
"title": f"Ad: {ad.get('name', 'Unnamed Ad')}",
|
|
210
|
+
"text": f"Meta Ad {ad.get('name', 'Unnamed')} (ID: {ad.get('id', 'N/A')}) - Status: {ad.get('status', 'Unknown')}, Bid Amount: ${ad.get('bid_amount', 'Not set')}, Account: {account.get('name', 'Unknown')}",
|
|
211
|
+
"metadata": {
|
|
212
|
+
"ad_id": ad.get('id'),
|
|
213
|
+
"ad_name": ad.get('name'),
|
|
214
|
+
"status": ad.get('status'),
|
|
215
|
+
"account_id": account.get('id'),
|
|
216
|
+
"account_name": account.get('name'),
|
|
217
|
+
"data_type": "meta_ads_ad"
|
|
218
|
+
},
|
|
219
|
+
"raw_data": ad
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
# If query specifically mentions "page" or "pages", also search pages
|
|
223
|
+
if any(term in ['page', 'pages', 'facebook page'] for term in query_terms):
|
|
224
|
+
for account in accounts[:5]: # Limit to first 5 accounts for performance
|
|
225
|
+
pages = await self._get_pages_for_account(access_token, account['id'])
|
|
226
|
+
for page in pages:
|
|
227
|
+
page_text = f"{page.get('name', '')} {page.get('source', '')}".lower()
|
|
228
|
+
|
|
229
|
+
if any(term in page_text for term in query_terms):
|
|
230
|
+
page_record_id = f"page:{page['id']}"
|
|
231
|
+
matching_ids.append(page_record_id)
|
|
232
|
+
|
|
233
|
+
# Cache the page data
|
|
234
|
+
self._cache[page_record_id] = {
|
|
235
|
+
"id": page_record_id,
|
|
236
|
+
"type": "page",
|
|
237
|
+
"title": f"Facebook Page: {page.get('name', 'Unnamed Page')}",
|
|
238
|
+
"text": f"Facebook Page {page.get('name', 'Unnamed')} (ID: {page.get('id', 'N/A')}) - Source: {page.get('source', 'Unknown')}, Account: {account.get('name', 'Unknown')}",
|
|
239
|
+
"metadata": {
|
|
240
|
+
"page_id": page.get('id'),
|
|
241
|
+
"page_name": page.get('name'),
|
|
242
|
+
"source": page.get('source'),
|
|
243
|
+
"account_id": account.get('id'),
|
|
244
|
+
"account_name": account.get('name'),
|
|
245
|
+
"data_type": "meta_ads_page"
|
|
246
|
+
},
|
|
247
|
+
"raw_data": page
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
# If query specifically mentions "business" or "businesses", also search businesses
|
|
251
|
+
if any(term in ['business', 'businesses', 'company', 'companies'] for term in query_terms):
|
|
252
|
+
businesses = await self._get_businesses(access_token, limit=25)
|
|
253
|
+
for business in businesses:
|
|
254
|
+
business_text = f"{business.get('name', '')} {business.get('verification_status', '')}".lower()
|
|
255
|
+
|
|
256
|
+
if any(term in business_text for term in query_terms):
|
|
257
|
+
business_record_id = f"business:{business['id']}"
|
|
258
|
+
matching_ids.append(business_record_id)
|
|
259
|
+
|
|
260
|
+
# Cache the business data
|
|
261
|
+
self._cache[business_record_id] = {
|
|
262
|
+
"id": business_record_id,
|
|
263
|
+
"type": "business",
|
|
264
|
+
"title": f"Business: {business.get('name', 'Unnamed Business')}",
|
|
265
|
+
"text": f"Meta Business {business.get('name', 'Unnamed')} (ID: {business.get('id', 'N/A')}) - Created: {business.get('created_time', 'Unknown')}, Verification: {business.get('verification_status', 'Unknown')}",
|
|
266
|
+
"metadata": {
|
|
267
|
+
"business_id": business.get('id'),
|
|
268
|
+
"business_name": business.get('name'),
|
|
269
|
+
"created_time": business.get('created_time'),
|
|
270
|
+
"verification_status": business.get('verification_status'),
|
|
271
|
+
"data_type": "meta_ads_business"
|
|
272
|
+
},
|
|
273
|
+
"raw_data": business
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
except Exception as e:
|
|
277
|
+
logger.error(f"Error during search operation: {e}")
|
|
278
|
+
# Return empty list on error, but don't raise exception
|
|
279
|
+
return []
|
|
280
|
+
|
|
281
|
+
logger.info(f"Search completed. Found {len(matching_ids)} matching records")
|
|
282
|
+
return matching_ids[:50] # Limit to 50 results for performance
|
|
283
|
+
|
|
284
|
+
def fetch_record(self, record_id: str) -> Optional[Dict[str, Any]]:
|
|
285
|
+
"""Fetch a cached record by ID
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
record_id: The record ID to fetch
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
Record data or None if not found
|
|
292
|
+
"""
|
|
293
|
+
logger.info(f"Fetching record: {record_id}")
|
|
294
|
+
|
|
295
|
+
record = self._cache.get(record_id)
|
|
296
|
+
if record:
|
|
297
|
+
logger.debug(f"Record found in cache: {record['type']}")
|
|
298
|
+
return record
|
|
299
|
+
else:
|
|
300
|
+
logger.warning(f"Record not found in cache: {record_id}")
|
|
301
|
+
return None
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# Global data manager instance
|
|
305
|
+
_data_manager = MetaAdsDataManager()
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@mcp_server.tool()
|
|
309
|
+
@meta_api_tool
|
|
310
|
+
async def search(
|
|
311
|
+
query: str,
|
|
312
|
+
access_token: Optional[str] = None
|
|
313
|
+
) -> str:
|
|
314
|
+
"""
|
|
315
|
+
Search through Meta Ads data and return matching record IDs.
|
|
316
|
+
It searches across ad accounts, campaigns, ads, pages, and businesses to find relevant records
|
|
317
|
+
based on the provided query.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
query: Search query string to find relevant Meta Ads records
|
|
321
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
JSON response with list of matching record IDs
|
|
325
|
+
|
|
326
|
+
Example Usage:
|
|
327
|
+
search(query="active campaigns")
|
|
328
|
+
search(query="account spending")
|
|
329
|
+
search(query="facebook ads performance")
|
|
330
|
+
search(query="facebook pages")
|
|
331
|
+
search(query="user businesses")
|
|
332
|
+
"""
|
|
333
|
+
if not query:
|
|
334
|
+
return json.dumps({
|
|
335
|
+
"error": "query parameter is required",
|
|
336
|
+
"ids": []
|
|
337
|
+
}, indent=2)
|
|
338
|
+
|
|
339
|
+
try:
|
|
340
|
+
# Use the data manager to search records
|
|
341
|
+
matching_ids = await _data_manager.search_records(query, access_token)
|
|
342
|
+
|
|
343
|
+
response = {
|
|
344
|
+
"ids": matching_ids,
|
|
345
|
+
"query": query,
|
|
346
|
+
"total_results": len(matching_ids)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
logger.info(f"Search successful. Query: '{query}', Results: {len(matching_ids)}")
|
|
350
|
+
return json.dumps(response, indent=2)
|
|
351
|
+
|
|
352
|
+
except Exception as e:
|
|
353
|
+
error_msg = str(e)
|
|
354
|
+
logger.error(f"Error in search tool: {error_msg}")
|
|
355
|
+
|
|
356
|
+
return json.dumps({
|
|
357
|
+
"error": "Failed to search Meta Ads data",
|
|
358
|
+
"details": error_msg,
|
|
359
|
+
"ids": [],
|
|
360
|
+
"query": query
|
|
361
|
+
}, indent=2)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
@mcp_server.tool()
|
|
365
|
+
async def fetch(
|
|
366
|
+
id: str
|
|
367
|
+
) -> str:
|
|
368
|
+
"""
|
|
369
|
+
Fetch a record previously returned by the 'search' tool in the same session.
|
|
370
|
+
|
|
371
|
+
IMPORTANT LIMITATIONS:
|
|
372
|
+
- This tool ONLY returns records that were cached by a prior 'search' call.
|
|
373
|
+
It does NOT make direct API calls to Meta. If the record was not found by
|
|
374
|
+
'search' first, this tool will return "Record not found".
|
|
375
|
+
- Do NOT use this tool to look up campaigns, adsets, or ads by ID directly.
|
|
376
|
+
|
|
377
|
+
For direct lookups by ID, use these tools instead:
|
|
378
|
+
- get_campaign_details(campaign_id=...) - for campaigns
|
|
379
|
+
- get_adset_details(adset_id=...) - for ad sets
|
|
380
|
+
- get_ads(account_id=..., campaign_id=...) - for ads
|
|
381
|
+
- get_adsets(account_id=..., campaign_id=...) - for ad sets in a campaign
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
id: The record ID to fetch (format: "type:id", e.g., "account:act_123456").
|
|
385
|
+
Must have been returned by a previous 'search' call.
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
JSON response with record data, or "Record not found" if the record
|
|
389
|
+
was not previously cached by 'search'.
|
|
390
|
+
"""
|
|
391
|
+
if not id:
|
|
392
|
+
return json.dumps({
|
|
393
|
+
"error": "id parameter is required"
|
|
394
|
+
}, indent=2)
|
|
395
|
+
|
|
396
|
+
try:
|
|
397
|
+
# Use the data manager to fetch the record
|
|
398
|
+
record = _data_manager.fetch_record(id)
|
|
399
|
+
|
|
400
|
+
if record:
|
|
401
|
+
logger.info(f"Record fetched successfully: {id}")
|
|
402
|
+
return json.dumps(record, indent=2)
|
|
403
|
+
else:
|
|
404
|
+
logger.warning(f"Record not found: {id}")
|
|
405
|
+
return json.dumps({
|
|
406
|
+
"error": f"Record not found: {id}",
|
|
407
|
+
"id": id
|
|
408
|
+
}, indent=2)
|
|
409
|
+
|
|
410
|
+
except Exception as e:
|
|
411
|
+
error_msg = str(e)
|
|
412
|
+
logger.error(f"Error in fetch tool: {error_msg}")
|
|
413
|
+
|
|
414
|
+
return json.dumps({
|
|
415
|
+
"error": "Failed to fetch record",
|
|
416
|
+
"details": error_msg,
|
|
417
|
+
"id": id
|
|
418
|
+
}, indent=2)
|