meta-ads-mcp 0.5.0__tar.gz → 0.6.0__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 (56) hide show
  1. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/PKG-INFO +1 -1
  2. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/meta_ads_mcp/__init__.py +1 -1
  3. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/meta_ads_mcp/core/__init__.py +3 -0
  4. meta_ads_mcp-0.6.0/meta_ads_mcp/core/openai_deep_research.py +316 -0
  5. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/meta_ads_mcp/core/server.py +6 -1
  6. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/pyproject.toml +1 -1
  7. meta_ads_mcp-0.6.0/tests/test_account_search.py +336 -0
  8. meta_ads_mcp-0.6.0/tests/test_integration_openai_mcp.py +242 -0
  9. meta_ads_mcp-0.6.0/tests/test_openai_mcp_deep_research.py +381 -0
  10. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/.github/workflows/publish.yml +0 -0
  11. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/.github/workflows/test.yml +0 -0
  12. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/.gitignore +0 -0
  13. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/CUSTOM_META_APP.md +0 -0
  14. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/Dockerfile +0 -0
  15. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/LICENSE +0 -0
  16. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/LOCAL_INSTALLATION.md +0 -0
  17. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/META_API_NOTES.md +0 -0
  18. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/README.md +0 -0
  19. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/RELEASE.md +0 -0
  20. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/STREAMABLE_HTTP_SETUP.md +0 -0
  21. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/examples/README.md +0 -0
  22. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/examples/example_http_client.py +0 -0
  23. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/future_improvements.md +0 -0
  24. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/images/meta-ads-example.png +0 -0
  25. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/meta_ads_auth.sh +0 -0
  26. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/meta_ads_mcp/__main__.py +0 -0
  27. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/meta_ads_mcp/core/accounts.py +0 -0
  28. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/meta_ads_mcp/core/ads.py +0 -0
  29. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/meta_ads_mcp/core/ads_library.py +0 -0
  30. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/meta_ads_mcp/core/adsets.py +0 -0
  31. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/meta_ads_mcp/core/api.py +0 -0
  32. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/meta_ads_mcp/core/auth.py +0 -0
  33. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/meta_ads_mcp/core/authentication.py +0 -0
  34. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/meta_ads_mcp/core/budget_schedules.py +0 -0
  35. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/meta_ads_mcp/core/callback_server.py +0 -0
  36. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/meta_ads_mcp/core/campaigns.py +0 -0
  37. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/meta_ads_mcp/core/duplication.py +0 -0
  38. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/meta_ads_mcp/core/http_auth_integration.py +0 -0
  39. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/meta_ads_mcp/core/insights.py +0 -0
  40. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/meta_ads_mcp/core/pipeboard_auth.py +0 -0
  41. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/meta_ads_mcp/core/reports.py +0 -0
  42. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/meta_ads_mcp/core/resources.py +0 -0
  43. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/meta_ads_mcp/core/utils.py +0 -0
  44. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/requirements.txt +0 -0
  45. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/setup.py +0 -0
  46. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/smithery.yaml +0 -0
  47. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/tests/README.md +0 -0
  48. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/tests/README_REGRESSION_TESTS.md +0 -0
  49. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/tests/__init__.py +0 -0
  50. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/tests/conftest.py +0 -0
  51. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/tests/test_duplication.py +0 -0
  52. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/tests/test_duplication_regression.py +0 -0
  53. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/tests/test_get_ad_creatives_fix.py +0 -0
  54. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/tests/test_get_ad_image_regression.py +0 -0
  55. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/tests/test_http_transport.py +0 -0
  56. {meta_ads_mcp-0.5.0 → meta_ads_mcp-0.6.0}/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.5.0
3
+ Version: 0.6.0
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
@@ -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.5.0"
10
+ __version__ = "0.6.0"
11
11
 
12
12
  __all__ = [
13
13
  'get_ad_accounts',
@@ -13,6 +13,7 @@ from .ads_library import search_ads_archive
13
13
  from .budget_schedules import create_budget_schedule
14
14
  from . import reports # Import module to register conditional tools
15
15
  from . import duplication # Import module to register conditional duplication tools
16
+ from .openai_deep_research import search, fetch # OpenAI MCP Deep Research tools
16
17
 
17
18
  __all__ = [
18
19
  'mcp_server',
@@ -36,4 +37,6 @@ __all__ = [
36
37
  'main',
37
38
  'search_ads_archive',
38
39
  'create_budget_schedule',
40
+ 'search', # OpenAI MCP Deep Research search tool
41
+ 'fetch', # OpenAI MCP Deep Research fetch tool
39
42
  ]
@@ -0,0 +1,316 @@
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
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 = 25) -> 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 search_records(self, query: str, access_token: str) -> List[str]:
81
+ """Search Meta Ads data and return matching record IDs
82
+
83
+ Args:
84
+ query: Search query string
85
+ access_token: Meta API access token
86
+
87
+ Returns:
88
+ List of record IDs that match the query
89
+ """
90
+ logger.info(f"Searching Meta Ads data with query: {query}")
91
+
92
+ # Normalize query for matching
93
+ query_lower = query.lower()
94
+ query_terms = re.findall(r'\w+', query_lower)
95
+
96
+ matching_ids = []
97
+
98
+ try:
99
+ # Search ad accounts
100
+ accounts = await self._get_ad_accounts(access_token, limit=25)
101
+ for account in accounts:
102
+ account_text = f"{account.get('name', '')} {account.get('id', '')} {account.get('account_status', '')} {account.get('business_city', '')} {account.get('business_country_code', '')}".lower()
103
+
104
+ if any(term in account_text for term in query_terms):
105
+ record_id = f"account:{account['id']}"
106
+ matching_ids.append(record_id)
107
+
108
+ # Cache the account data
109
+ self._cache[record_id] = {
110
+ "id": record_id,
111
+ "type": "account",
112
+ "title": f"Ad Account: {account.get('name', 'Unnamed Account')}",
113
+ "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)}",
114
+ "metadata": {
115
+ "account_id": account.get('id'),
116
+ "account_name": account.get('name'),
117
+ "status": account.get('account_status'),
118
+ "currency": account.get('currency'),
119
+ "business_location": f"{account.get('business_city', '')}, {account.get('business_country_code', '')}".strip(', '),
120
+ "data_type": "meta_ads_account"
121
+ },
122
+ "raw_data": account
123
+ }
124
+
125
+ # Also search campaigns for this account if it matches
126
+ campaigns = await self._get_campaigns(access_token, account['id'], limit=10)
127
+ for campaign in campaigns:
128
+ campaign_text = f"{campaign.get('name', '')} {campaign.get('objective', '')} {campaign.get('status', '')}".lower()
129
+
130
+ if any(term in campaign_text for term in query_terms):
131
+ campaign_record_id = f"campaign:{campaign['id']}"
132
+ matching_ids.append(campaign_record_id)
133
+
134
+ # Cache the campaign data
135
+ self._cache[campaign_record_id] = {
136
+ "id": campaign_record_id,
137
+ "type": "campaign",
138
+ "title": f"Campaign: {campaign.get('name', 'Unnamed Campaign')}",
139
+ "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')}",
140
+ "metadata": {
141
+ "campaign_id": campaign.get('id'),
142
+ "campaign_name": campaign.get('name'),
143
+ "objective": campaign.get('objective'),
144
+ "status": campaign.get('status'),
145
+ "account_id": account.get('id'),
146
+ "account_name": account.get('name'),
147
+ "data_type": "meta_ads_campaign"
148
+ },
149
+ "raw_data": campaign
150
+ }
151
+
152
+ # If query specifically mentions "ads" or "ad", also search individual ads
153
+ if any(term in ['ad', 'ads', 'advertisement', 'creative'] for term in query_terms):
154
+ for account in accounts[:3]: # Limit to first 3 accounts for performance
155
+ ads = await self._get_ads(access_token, account['id'], limit=10)
156
+ for ad in ads:
157
+ ad_text = f"{ad.get('name', '')} {ad.get('status', '')}".lower()
158
+
159
+ if any(term in ad_text for term in query_terms):
160
+ ad_record_id = f"ad:{ad['id']}"
161
+ matching_ids.append(ad_record_id)
162
+
163
+ # Cache the ad data
164
+ self._cache[ad_record_id] = {
165
+ "id": ad_record_id,
166
+ "type": "ad",
167
+ "title": f"Ad: {ad.get('name', 'Unnamed Ad')}",
168
+ "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')}",
169
+ "metadata": {
170
+ "ad_id": ad.get('id'),
171
+ "ad_name": ad.get('name'),
172
+ "status": ad.get('status'),
173
+ "account_id": account.get('id'),
174
+ "account_name": account.get('name'),
175
+ "data_type": "meta_ads_ad"
176
+ },
177
+ "raw_data": ad
178
+ }
179
+
180
+ except Exception as e:
181
+ logger.error(f"Error during search operation: {e}")
182
+ # Return empty list on error, but don't raise exception
183
+ return []
184
+
185
+ logger.info(f"Search completed. Found {len(matching_ids)} matching records")
186
+ return matching_ids[:50] # Limit to 50 results for performance
187
+
188
+ def fetch_record(self, record_id: str) -> Optional[Dict[str, Any]]:
189
+ """Fetch a cached record by ID
190
+
191
+ Args:
192
+ record_id: The record ID to fetch
193
+
194
+ Returns:
195
+ Record data or None if not found
196
+ """
197
+ logger.info(f"Fetching record: {record_id}")
198
+
199
+ record = self._cache.get(record_id)
200
+ if record:
201
+ logger.debug(f"Record found in cache: {record['type']}")
202
+ return record
203
+ else:
204
+ logger.warning(f"Record not found in cache: {record_id}")
205
+ return None
206
+
207
+
208
+ # Global data manager instance
209
+ _data_manager = MetaAdsDataManager()
210
+
211
+
212
+ @mcp_server.tool()
213
+ @meta_api_tool
214
+ async def search(
215
+ access_token: str = None,
216
+ query: str = None
217
+ ) -> str:
218
+ """
219
+ Search through Meta Ads data and return matching record IDs.
220
+
221
+ This tool is required for OpenAI ChatGPT Deep Research integration.
222
+ It searches across ad accounts, campaigns, and ads to find relevant records
223
+ based on the provided query.
224
+
225
+ Args:
226
+ access_token: Meta API access token (optional - will use cached token if not provided)
227
+ query: Search query string to find relevant Meta Ads records
228
+
229
+ Returns:
230
+ JSON response with list of matching record IDs
231
+
232
+ Example Usage:
233
+ search(query="active campaigns")
234
+ search(query="account spending")
235
+ search(query="facebook ads performance")
236
+ """
237
+ if not query:
238
+ return json.dumps({
239
+ "error": "query parameter is required",
240
+ "ids": []
241
+ }, indent=2)
242
+
243
+ try:
244
+ # Use the data manager to search records
245
+ matching_ids = await _data_manager.search_records(query, access_token)
246
+
247
+ response = {
248
+ "ids": matching_ids,
249
+ "query": query,
250
+ "total_results": len(matching_ids)
251
+ }
252
+
253
+ logger.info(f"Search successful. Query: '{query}', Results: {len(matching_ids)}")
254
+ return json.dumps(response, indent=2)
255
+
256
+ except Exception as e:
257
+ error_msg = str(e)
258
+ logger.error(f"Error in search tool: {error_msg}")
259
+
260
+ return json.dumps({
261
+ "error": "Failed to search Meta Ads data",
262
+ "details": error_msg,
263
+ "ids": [],
264
+ "query": query
265
+ }, indent=2)
266
+
267
+
268
+ @mcp_server.tool()
269
+ async def fetch(
270
+ id: str = None
271
+ ) -> str:
272
+ """
273
+ Fetch complete record data by ID.
274
+
275
+ This tool is required for OpenAI ChatGPT Deep Research integration.
276
+ It retrieves the full data for a specific record identified by its ID.
277
+
278
+ Args:
279
+ id: The record ID to fetch (format: "type:id", e.g., "account:act_123456")
280
+
281
+ Returns:
282
+ JSON response with complete record data including id, title, text, and metadata
283
+
284
+ Example Usage:
285
+ fetch(id="account:act_123456789")
286
+ fetch(id="campaign:23842588888640185")
287
+ fetch(id="ad:23842614006130185")
288
+ """
289
+ if not id:
290
+ return json.dumps({
291
+ "error": "id parameter is required"
292
+ }, indent=2)
293
+
294
+ try:
295
+ # Use the data manager to fetch the record
296
+ record = _data_manager.fetch_record(id)
297
+
298
+ if record:
299
+ logger.info(f"Record fetched successfully: {id}")
300
+ return json.dumps(record, indent=2)
301
+ else:
302
+ logger.warning(f"Record not found: {id}")
303
+ return json.dumps({
304
+ "error": f"Record not found: {id}",
305
+ "id": id
306
+ }, indent=2)
307
+
308
+ except Exception as e:
309
+ error_msg = str(e)
310
+ logger.error(f"Error in fetch tool: {error_msg}")
311
+
312
+ return json.dumps({
313
+ "error": "Failed to fetch record",
314
+ "details": error_msg,
315
+ "id": id
316
+ }, indent=2)
@@ -276,6 +276,8 @@ def main():
276
276
  pipeboard_api_token = os.environ.get("PIPEBOARD_API_TOKEN")
277
277
  if pipeboard_api_token:
278
278
  logger.info("Using Pipeboard authentication")
279
+ print("✅ Pipeboard authentication enabled")
280
+ print(f" API token: {pipeboard_api_token[:8]}...{pipeboard_api_token[-4:]}")
279
281
  # Check for existing token
280
282
  token = pipeboard_auth_manager.get_access_token()
281
283
  if not token:
@@ -312,6 +314,9 @@ def main():
312
314
  except Exception as e:
313
315
  logger.error(f"Error initiating browser-based authentication: {e}")
314
316
  print(f"Error: Could not start authentication: {e}")
317
+ else:
318
+ print(f"✅ Valid Pipeboard access token found")
319
+ print(f" Token preview: {token[:10]}...{token[-5:]}")
315
320
 
316
321
  # Transport-specific server initialization and startup
317
322
  if args.transport == "streamable-http":
@@ -336,7 +341,7 @@ def main():
336
341
  # Import all tool modules to ensure they are registered
337
342
  logger.info("Ensuring all tools are registered for HTTP transport")
338
343
  from . import accounts, campaigns, adsets, ads, insights, authentication
339
- from . import ads_library, budget_schedules, reports
344
+ from . import ads_library, budget_schedules, reports, openai_deep_research
340
345
 
341
346
  # ✅ NEW: Setup HTTP authentication middleware
342
347
  logger.info("Setting up HTTP authentication middleware")
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "meta-ads-mcp"
7
- version = "0.5.0"
7
+ version = "0.6.0"
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"