meta-ads-mcp 0.4.9__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.
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/PKG-INFO +1 -1
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/meta_ads_mcp/__init__.py +1 -1
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/meta_ads_mcp/core/__init__.py +3 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/meta_ads_mcp/core/ads.py +47 -4
- meta_ads_mcp-0.6.0/meta_ads_mcp/core/openai_deep_research.py +316 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/meta_ads_mcp/core/server.py +6 -1
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/pyproject.toml +1 -1
- meta_ads_mcp-0.6.0/tests/test_account_search.py +336 -0
- meta_ads_mcp-0.6.0/tests/test_get_ad_image_regression.py +382 -0
- meta_ads_mcp-0.6.0/tests/test_integration_openai_mcp.py +242 -0
- meta_ads_mcp-0.6.0/tests/test_openai_mcp_deep_research.py +381 -0
- meta_ads_mcp-0.4.9/tests/test_get_ad_image_regression.py +0 -188
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/.github/workflows/publish.yml +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/.github/workflows/test.yml +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/.gitignore +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/CUSTOM_META_APP.md +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/Dockerfile +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/LICENSE +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/LOCAL_INSTALLATION.md +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/META_API_NOTES.md +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/README.md +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/RELEASE.md +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/STREAMABLE_HTTP_SETUP.md +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/examples/README.md +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/examples/example_http_client.py +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/future_improvements.md +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/images/meta-ads-example.png +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/meta_ads_auth.sh +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/meta_ads_mcp/__main__.py +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/meta_ads_mcp/core/accounts.py +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/meta_ads_mcp/core/ads_library.py +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/meta_ads_mcp/core/adsets.py +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/meta_ads_mcp/core/api.py +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/meta_ads_mcp/core/auth.py +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/meta_ads_mcp/core/authentication.py +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/meta_ads_mcp/core/budget_schedules.py +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/meta_ads_mcp/core/callback_server.py +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/meta_ads_mcp/core/campaigns.py +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/meta_ads_mcp/core/duplication.py +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/meta_ads_mcp/core/http_auth_integration.py +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/meta_ads_mcp/core/insights.py +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/meta_ads_mcp/core/pipeboard_auth.py +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/meta_ads_mcp/core/reports.py +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/meta_ads_mcp/core/resources.py +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/meta_ads_mcp/core/utils.py +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/requirements.txt +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/setup.py +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/smithery.yaml +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/tests/README.md +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/tests/README_REGRESSION_TESTS.md +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/tests/__init__.py +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/tests/conftest.py +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/tests/test_duplication.py +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/tests/test_duplication_regression.py +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/tests/test_get_ad_creatives_fix.py +0 -0
- {meta_ads_mcp-0.4.9 → meta_ads_mcp-0.6.0}/tests/test_http_transport.py +0 -0
- {meta_ads_mcp-0.4.9 → 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.
|
|
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
|
|
@@ -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
|
]
|
|
@@ -250,7 +250,7 @@ async def get_ad_image(access_token: str = None, ad_id: str = None) -> Image:
|
|
|
250
250
|
|
|
251
251
|
if not image_hashes:
|
|
252
252
|
# If no hashes found, try to extract from the first creative we found in the API
|
|
253
|
-
#
|
|
253
|
+
# and also check for direct URLs as fallback
|
|
254
254
|
creative_json = await get_ad_creatives(access_token=access_token, ad_id=ad_id)
|
|
255
255
|
creative_data = json.loads(creative_json)
|
|
256
256
|
|
|
@@ -270,9 +270,52 @@ async def get_ad_image(access_token: str = None, ad_id: str = None) -> Image:
|
|
|
270
270
|
images = creative["asset_feed_spec"]["images"]
|
|
271
271
|
if images and len(images) > 0 and "hash" in images[0]:
|
|
272
272
|
image_hashes.append(images[0]["hash"])
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
273
|
+
|
|
274
|
+
# If still no image hashes found, try direct URL fallback approach
|
|
275
|
+
if not image_hashes:
|
|
276
|
+
print("No image hashes found, trying direct URL fallback...")
|
|
277
|
+
|
|
278
|
+
image_url = None
|
|
279
|
+
if "data" in creative_data and creative_data["data"]:
|
|
280
|
+
creative = creative_data["data"][0]
|
|
281
|
+
|
|
282
|
+
# Try image_urls_for_viewing first (usually higher quality)
|
|
283
|
+
if "image_urls_for_viewing" in creative and creative["image_urls_for_viewing"]:
|
|
284
|
+
image_url = creative["image_urls_for_viewing"][0]
|
|
285
|
+
print(f"Using image_urls_for_viewing: {image_url}")
|
|
286
|
+
# Fall back to thumbnail_url
|
|
287
|
+
elif "thumbnail_url" in creative and creative["thumbnail_url"]:
|
|
288
|
+
image_url = creative["thumbnail_url"]
|
|
289
|
+
print(f"Using thumbnail_url: {image_url}")
|
|
290
|
+
|
|
291
|
+
if not image_url:
|
|
292
|
+
return "Error: No image URLs found in creative"
|
|
293
|
+
|
|
294
|
+
# Download the image directly
|
|
295
|
+
print(f"Downloading image from direct URL: {image_url}")
|
|
296
|
+
image_bytes = await download_image(image_url)
|
|
297
|
+
|
|
298
|
+
if not image_bytes:
|
|
299
|
+
return "Error: Failed to download image from direct URL"
|
|
300
|
+
|
|
301
|
+
try:
|
|
302
|
+
# Convert bytes to PIL Image
|
|
303
|
+
img = PILImage.open(io.BytesIO(image_bytes))
|
|
304
|
+
|
|
305
|
+
# Convert to RGB if needed
|
|
306
|
+
if img.mode != "RGB":
|
|
307
|
+
img = img.convert("RGB")
|
|
308
|
+
|
|
309
|
+
# Create a byte stream of the image data
|
|
310
|
+
byte_arr = io.BytesIO()
|
|
311
|
+
img.save(byte_arr, format="JPEG")
|
|
312
|
+
img_bytes = byte_arr.getvalue()
|
|
313
|
+
|
|
314
|
+
# Return as an Image object that LLM can directly analyze
|
|
315
|
+
return Image(data=img_bytes, format="jpeg")
|
|
316
|
+
|
|
317
|
+
except Exception as e:
|
|
318
|
+
return f"Error processing image from direct URL: {str(e)}"
|
|
276
319
|
|
|
277
320
|
print(f"Found image hashes: {image_hashes}")
|
|
278
321
|
|
|
@@ -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")
|