meta-ads-mcp 0.7.6__tar.gz → 0.7.8__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 (65) hide show
  1. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/PKG-INFO +1 -1
  2. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/RELEASE.md +23 -10
  3. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/meta_ads_mcp/__init__.py +1 -1
  4. meta_ads_mcp-0.7.8/meta_ads_mcp/core/accounts.py +101 -0
  5. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/meta_ads_mcp/core/adsets.py +36 -6
  6. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/meta_ads_mcp/core/auth.py +38 -2
  7. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/meta_ads_mcp/core/pipeboard_auth.py +43 -1
  8. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/pyproject.toml +1 -1
  9. meta_ads_mcp-0.7.8/tests/test_dsa_beneficiary.py +661 -0
  10. meta_ads_mcp-0.7.8/tests/test_dsa_integration.py +449 -0
  11. meta_ads_mcp-0.7.6/meta_ads_mcp/core/accounts.py +0 -62
  12. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/.github/workflows/publish.yml +0 -0
  13. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/.github/workflows/test.yml +0 -0
  14. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/.gitignore +0 -0
  15. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/CUSTOM_META_APP.md +0 -0
  16. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/Dockerfile +0 -0
  17. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/LICENSE +0 -0
  18. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/LOCAL_INSTALLATION.md +0 -0
  19. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/META_API_NOTES.md +0 -0
  20. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/README.md +0 -0
  21. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/STREAMABLE_HTTP_SETUP.md +0 -0
  22. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/examples/README.md +0 -0
  23. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/examples/example_http_client.py +0 -0
  24. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/future_improvements.md +0 -0
  25. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/images/meta-ads-example.png +0 -0
  26. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/meta_ads_auth.sh +0 -0
  27. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/meta_ads_mcp/__main__.py +0 -0
  28. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/meta_ads_mcp/core/__init__.py +0 -0
  29. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/meta_ads_mcp/core/ads.py +0 -0
  30. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/meta_ads_mcp/core/ads_library.py +0 -0
  31. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/meta_ads_mcp/core/api.py +0 -0
  32. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/meta_ads_mcp/core/authentication.py +0 -0
  33. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/meta_ads_mcp/core/budget_schedules.py +0 -0
  34. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/meta_ads_mcp/core/callback_server.py +0 -0
  35. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/meta_ads_mcp/core/campaigns.py +0 -0
  36. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/meta_ads_mcp/core/duplication.py +0 -0
  37. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/meta_ads_mcp/core/http_auth_integration.py +0 -0
  38. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/meta_ads_mcp/core/insights.py +0 -0
  39. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/meta_ads_mcp/core/openai_deep_research.py +0 -0
  40. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/meta_ads_mcp/core/reports.py +0 -0
  41. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/meta_ads_mcp/core/resources.py +0 -0
  42. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/meta_ads_mcp/core/server.py +0 -0
  43. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/meta_ads_mcp/core/targeting.py +0 -0
  44. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/meta_ads_mcp/core/utils.py +0 -0
  45. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/requirements.txt +0 -0
  46. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/setup.py +0 -0
  47. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/smithery.yaml +0 -0
  48. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/tests/README.md +0 -0
  49. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/tests/README_REGRESSION_TESTS.md +0 -0
  50. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/tests/__init__.py +0 -0
  51. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/tests/conftest.py +0 -0
  52. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/tests/test_account_search.py +0 -0
  53. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/tests/test_budget_update.py +0 -0
  54. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/tests/test_budget_update_e2e.py +0 -0
  55. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/tests/test_duplication.py +0 -0
  56. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/tests/test_duplication_regression.py +0 -0
  57. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/tests/test_get_ad_creatives_fix.py +0 -0
  58. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/tests/test_get_ad_image_regression.py +0 -0
  59. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/tests/test_http_transport.py +0 -0
  60. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/tests/test_insights_actions_and_values.py +0 -0
  61. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/tests/test_integration_openai_mcp.py +0 -0
  62. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/tests/test_openai.py +0 -0
  63. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/tests/test_openai_mcp_deep_research.py +0 -0
  64. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/tests/test_targeting.py +0 -0
  65. {meta_ads_mcp-0.7.6 → meta_ads_mcp-0.7.8}/tests/test_targeting_search_e2e.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meta-ads-mcp
3
- Version: 0.7.6
3
+ Version: 0.7.8
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
@@ -29,15 +29,27 @@ This repository uses GitHub Actions to automatically publish releases to PyPI. H
29
29
  git push origin main
30
30
  ```
31
31
 
32
- 3. **Create a GitHub release**:
33
- - Go to https://github.com/pipeboard-co/meta-ads-mcp/releases
34
- - Click "Create a new release"
35
- - Tag version: `v0.3.8` (must match the version in pyproject.toml)
36
- - Release title: `v0.3.8`
37
- - Add release notes describing what changed
38
- - Click "Publish release"
39
-
40
- 4. **Automatic deployment**:
32
+ 3. **Wait for build tests to pass** (optional):
33
+ ```bash
34
+ # Check the latest test workflow run
35
+ gh run list --workflow=test.yml --limit 1
36
+
37
+ # Get the run ID and wait for completion
38
+ RUN_ID=$(gh run list --workflow=test.yml --limit 1 --json databaseId --jq '.[0].databaseId')
39
+ gh run watch $RUN_ID
40
+ ```
41
+ Note: This only tests package building and installation, not the actual pytest tests.
42
+
43
+ 4. **Create a GitHub release**:
44
+ ```bash
45
+ gh release create 0.3.8 --title "0.3.8" --generate-notes
46
+ ```
47
+ This command will:
48
+ - Create a release with the specified version (no "v" prefix)
49
+ - Auto-generate release notes from commits
50
+ - Automatically trigger the GitHub Action for PyPI publishing
51
+
52
+ 5. **Automatic deployment**:
41
53
  - The GitHub Action will automatically trigger
42
54
  - It will build the package and publish to PyPI
43
55
  - Check the "Actions" tab to monitor progress
@@ -49,10 +61,11 @@ This repository uses GitHub Actions to automatically publish releases to PyPI. H
49
61
  - **Purpose**: Builds and publishes the package to PyPI
50
62
  - **Security**: Uses trusted publishing with OIDC tokens (no API keys needed)
51
63
 
52
- ### `test.yml` (if present)
64
+ ### `test.yml`
53
65
  - **Triggers**: On pushes and pull requests to main/master
54
66
  - **Purpose**: Tests package building and installation across Python versions
55
67
  - **Matrix**: Tests Python 3.10, 3.11, and 3.12
68
+ - **Note**: Does not run pytest tests, only validates package structure
56
69
 
57
70
  ## Manual Deployment
58
71
 
@@ -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.7.6"
10
+ __version__ = "0.7.8"
11
11
 
12
12
  __all__ = [
13
13
  'get_ad_accounts',
@@ -0,0 +1,101 @@
1
+ """Account-related functionality for Meta Ads API."""
2
+
3
+ import json
4
+ from typing import Optional
5
+ from .api import meta_api_tool, make_api_request
6
+ from .server import mcp_server
7
+
8
+
9
+ @mcp_server.tool()
10
+ @meta_api_tool
11
+ async def get_ad_accounts(access_token: str = None, user_id: str = "me", limit: int = 10) -> str:
12
+ """
13
+ Get ad accounts accessible by a user.
14
+
15
+ Args:
16
+ access_token: Meta API access token (optional - will use cached token if not provided)
17
+ user_id: Meta user ID or "me" for the current user
18
+ limit: Maximum number of accounts to return (default: 10)
19
+ """
20
+ endpoint = f"{user_id}/adaccounts"
21
+ params = {
22
+ "fields": "id,name,account_id,account_status,amount_spent,balance,currency,age,business_city,business_country_code",
23
+ "limit": limit
24
+ }
25
+
26
+ data = await make_api_request(endpoint, access_token, params)
27
+
28
+ return json.dumps(data, indent=2)
29
+
30
+
31
+ @mcp_server.tool()
32
+ @meta_api_tool
33
+ async def get_account_info(access_token: str = None, account_id: str = None) -> str:
34
+ """
35
+ Get detailed information about a specific ad account.
36
+
37
+ Args:
38
+ access_token: Meta API access token (optional - will use cached token if not provided)
39
+ account_id: Meta Ads account ID (format: act_XXXXXXXXX) - REQUIRED
40
+ """
41
+ if not account_id:
42
+ return {
43
+ "error": {
44
+ "message": "Account ID is required",
45
+ "details": "Please specify an account_id parameter",
46
+ "example": "Use account_id='act_123456789' or account_id='123456789'"
47
+ }
48
+ }
49
+
50
+ # Ensure account_id has the 'act_' prefix for API compatibility
51
+ if not account_id.startswith("act_"):
52
+ account_id = f"act_{account_id}"
53
+
54
+ # First, check if the account is accessible to the user
55
+ endpoint = "me/adaccounts"
56
+ params = {
57
+ "fields": "id,name,account_id,account_status,amount_spent,balance,currency,age,business_city,business_country_code",
58
+ "limit": 50
59
+ }
60
+ accessible_accounts_data = await make_api_request(endpoint, access_token, params)
61
+
62
+ if "data" in accessible_accounts_data:
63
+ accessible_account_ids = [acc["id"] for acc in accessible_accounts_data["data"]]
64
+ if account_id not in accessible_account_ids:
65
+ # Provide a helpful error message with accessible accounts
66
+ accessible_accounts = [
67
+ {"id": acc["id"], "name": acc["name"]}
68
+ for acc in accessible_accounts_data["data"][:10] # Show first 10
69
+ ]
70
+ return {
71
+ "error": {
72
+ "message": f"Account {account_id} is not accessible to your user account",
73
+ "details": "This account either doesn't exist or you don't have permission to access it",
74
+ "accessible_accounts": accessible_accounts,
75
+ "total_accessible_accounts": len(accessible_accounts_data["data"]),
76
+ "suggestion": "Try using one of the accessible account IDs listed above"
77
+ }
78
+ }
79
+
80
+ endpoint = f"{account_id}"
81
+ params = {
82
+ "fields": "id,name,account_id,account_status,amount_spent,balance,currency,age,business_city,business_country_code,timezone_name"
83
+ }
84
+
85
+ data = await make_api_request(endpoint, access_token, params)
86
+
87
+ # Check if the API request returned an error
88
+ if "error" in data:
89
+ return data
90
+
91
+ # Add DSA requirement detection
92
+ if "business_country_code" in data:
93
+ european_countries = ["DE", "FR", "IT", "ES", "NL", "BE", "AT", "IE", "DK", "SE", "FI", "NO"]
94
+ if data["business_country_code"] in european_countries:
95
+ data["dsa_required"] = True
96
+ data["dsa_compliance_note"] = "This account is subject to European DSA (Digital Services Act) requirements"
97
+ else:
98
+ data["dsa_required"] = False
99
+ data["dsa_compliance_note"] = "This account is not subject to European DSA requirements"
100
+
101
+ return data
@@ -73,7 +73,7 @@ async def get_adset_details(access_token: str = None, adset_id: str = None) -> s
73
73
  endpoint = f"{adset_id}"
74
74
  # Explicitly prioritize frequency_control_specs in the fields request
75
75
  params = {
76
- "fields": "id,name,campaign_id,status,frequency_control_specs{event,interval_days,max_frequency},daily_budget,lifetime_budget,targeting,bid_amount,bid_strategy,optimization_goal,billing_event,start_time,end_time,created_time,updated_time,attribution_spec,destination_type,promoted_object,pacing_type,budget_remaining"
76
+ "fields": "id,name,campaign_id,status,frequency_control_specs{event,interval_days,max_frequency},daily_budget,lifetime_budget,targeting,bid_amount,bid_strategy,optimization_goal,billing_event,start_time,end_time,created_time,updated_time,attribution_spec,destination_type,promoted_object,pacing_type,budget_remaining,dsa_beneficiary"
77
77
  }
78
78
 
79
79
  data = await make_api_request(endpoint, access_token, params)
@@ -103,6 +103,7 @@ async def create_adset(
103
103
  bid_strategy: str = None,
104
104
  start_time: str = None,
105
105
  end_time: str = None,
106
+ dsa_beneficiary: str = None,
106
107
  access_token: str = None
107
108
  ) -> str:
108
109
  """
@@ -123,6 +124,7 @@ async def create_adset(
123
124
  bid_strategy: Bid strategy (e.g., 'LOWEST_COST', 'LOWEST_COST_WITH_BID_CAP')
124
125
  start_time: Start time in ISO 8601 format (e.g., '2023-12-01T12:00:00-0800')
125
126
  end_time: End time in ISO 8601 format
127
+ dsa_beneficiary: DSA beneficiary (person/organization benefiting from ads) for European compliance
126
128
  access_token: Meta API access token (optional - will use cached token if not provided)
127
129
  """
128
130
  # Check required parameters
@@ -181,16 +183,44 @@ async def create_adset(
181
183
  if end_time:
182
184
  params["end_time"] = end_time
183
185
 
186
+ # Add DSA beneficiary if provided
187
+ if dsa_beneficiary:
188
+ params["dsa_beneficiary"] = dsa_beneficiary
189
+
184
190
  try:
185
191
  data = await make_api_request(endpoint, access_token, params, method="POST")
186
192
  return json.dumps(data, indent=2)
187
193
  except Exception as e:
188
194
  error_msg = str(e)
189
- return json.dumps({
190
- "error": "Failed to create ad set",
191
- "details": error_msg,
192
- "params_sent": params
193
- }, indent=2)
195
+
196
+ # Enhanced error handling for DSA beneficiary issues
197
+ if "permission" in error_msg.lower() or "insufficient" in error_msg.lower():
198
+ return json.dumps({
199
+ "error": "Insufficient permissions to set DSA beneficiary. Please ensure you have business_management permissions.",
200
+ "details": error_msg,
201
+ "params_sent": params,
202
+ "permission_required": True
203
+ }, indent=2)
204
+ elif "dsa_beneficiary" in error_msg.lower() and ("not supported" in error_msg.lower() or "parameter" in error_msg.lower()):
205
+ return json.dumps({
206
+ "error": "DSA beneficiary parameter not supported in this API version. Please set DSA beneficiary manually in Facebook Ads Manager.",
207
+ "details": error_msg,
208
+ "params_sent": params,
209
+ "manual_setup_required": True
210
+ }, indent=2)
211
+ elif "benefits from ads" in error_msg or "DSA beneficiary" in error_msg:
212
+ return json.dumps({
213
+ "error": "DSA beneficiary required for European compliance. Please provide the person or organization that benefits from ads in this ad set.",
214
+ "details": error_msg,
215
+ "params_sent": params,
216
+ "dsa_required": True
217
+ }, indent=2)
218
+ else:
219
+ return json.dumps({
220
+ "error": "Failed to create ad set",
221
+ "details": error_msg,
222
+ "params_sent": params
223
+ }, indent=2)
194
224
 
195
225
 
196
226
  @mcp_server.tool()
@@ -25,7 +25,7 @@ from .pipeboard_auth import pipeboard_auth_manager
25
25
  # Auth constants
26
26
  # Scope includes pages_show_list and pages_read_engagement to fix issue #16
27
27
  # where get_account_pages failed for regular users due to missing page permissions
28
- AUTH_SCOPE = "ads_management,ads_read,business_management,public_profile,pages_show_list,pages_read_engagement"
28
+ AUTH_SCOPE = "business_management,public_profile,pages_show_list,pages_read_engagement"
29
29
  AUTH_REDIRECT_URI = "http://localhost:8888/callback"
30
30
  AUTH_RESPONSE_TYPE = "token"
31
31
 
@@ -159,11 +159,41 @@ class AuthManager:
159
159
  try:
160
160
  with open(cache_path, "r") as f:
161
161
  data = json.load(f)
162
+
163
+ # Validate the cached data structure
164
+ required_fields = ["access_token", "created_at"]
165
+ if not all(field in data for field in required_fields):
166
+ logger.warning("Cached token data is missing required fields")
167
+ return False
168
+
169
+ # Check if the token looks valid (basic format check)
170
+ if not data.get("access_token") or len(data["access_token"]) < 20:
171
+ logger.warning("Cached token appears malformed")
172
+ return False
173
+
162
174
  self.token_info = TokenInfo.deserialize(data)
163
175
 
164
176
  # Check if token is expired
165
177
  if self.token_info.is_expired():
166
- logger.info("Cached token is expired")
178
+ logger.info("Cached token is expired, removing cache file")
179
+ # Remove the expired cache file
180
+ try:
181
+ cache_path.unlink()
182
+ logger.info(f"Removed expired token cache: {cache_path}")
183
+ except Exception as e:
184
+ logger.warning(f"Could not remove expired cache file: {e}")
185
+ self.token_info = None
186
+ return False
187
+
188
+ # Additional validation: check if token is too old (more than 60 days)
189
+ current_time = int(time.time())
190
+ if self.token_info.created_at and (current_time - self.token_info.created_at) > (60 * 24 * 3600):
191
+ logger.warning("Cached token is too old (more than 60 days), removing cache file")
192
+ try:
193
+ cache_path.unlink()
194
+ logger.info(f"Removed old token cache: {cache_path}")
195
+ except Exception as e:
196
+ logger.warning(f"Could not remove old cache file: {e}")
167
197
  self.token_info = None
168
198
  return False
169
199
 
@@ -171,6 +201,12 @@ class AuthManager:
171
201
  return True
172
202
  except Exception as e:
173
203
  logger.error(f"Error loading cached token: {e}")
204
+ # If there's any error reading the cache, try to remove the corrupted file
205
+ try:
206
+ cache_path.unlink()
207
+ logger.info(f"Removed corrupted token cache: {cache_path}")
208
+ except Exception as cleanup_error:
209
+ logger.warning(f"Could not remove corrupted cache file: {cleanup_error}")
174
210
  return False
175
211
 
176
212
  def _save_token_to_cache(self) -> None:
@@ -151,6 +151,18 @@ class PipeboardAuthManager:
151
151
  with open(cache_path, "r") as f:
152
152
  logger.debug(f"Reading token cache from {cache_path}")
153
153
  data = json.load(f)
154
+
155
+ # Validate the cached data structure
156
+ required_fields = ["access_token"]
157
+ if not all(field in data for field in required_fields):
158
+ logger.warning("Cached token data is missing required fields")
159
+ return False
160
+
161
+ # Check if the token looks valid (basic format check)
162
+ if not data.get("access_token") or len(data["access_token"]) < 20:
163
+ logger.warning("Cached token appears malformed")
164
+ return False
165
+
154
166
  self.token_info = TokenInfo.deserialize(data)
155
167
 
156
168
  # Log token details (partial token for security)
@@ -159,7 +171,25 @@ class PipeboardAuthManager:
159
171
 
160
172
  # Check if token is expired
161
173
  if self.token_info.is_expired():
162
- logger.info("Cached token is expired")
174
+ logger.info("Cached token is expired, removing cache file")
175
+ # Remove the expired cache file
176
+ try:
177
+ cache_path.unlink()
178
+ logger.info(f"Removed expired token cache: {cache_path}")
179
+ except Exception as e:
180
+ logger.warning(f"Could not remove expired cache file: {e}")
181
+ self.token_info = None
182
+ return False
183
+
184
+ # Additional validation: check if token is too old (more than 60 days)
185
+ current_time = int(time.time())
186
+ if self.token_info.created_at and (current_time - self.token_info.created_at) > (60 * 24 * 3600):
187
+ logger.warning("Cached token is too old (more than 60 days), removing cache file")
188
+ try:
189
+ cache_path.unlink()
190
+ logger.info(f"Removed old token cache: {cache_path}")
191
+ except Exception as e:
192
+ logger.warning(f"Could not remove old cache file: {e}")
163
193
  self.token_info = None
164
194
  return False
165
195
 
@@ -174,9 +204,21 @@ class PipeboardAuthManager:
174
204
  logger.debug(f"Raw cache file content (first 100 chars): {raw_content[:100]}")
175
205
  except Exception as e2:
176
206
  logger.error(f"Could not read raw cache file: {e2}")
207
+ # If there's any error reading the cache, try to remove the corrupted file
208
+ try:
209
+ cache_path.unlink()
210
+ logger.info(f"Removed corrupted token cache: {cache_path}")
211
+ except Exception as cleanup_error:
212
+ logger.warning(f"Could not remove corrupted cache file: {cleanup_error}")
177
213
  return False
178
214
  except Exception as e:
179
215
  logger.error(f"Error loading cached token: {e}")
216
+ # If there's any error reading the cache, try to remove the corrupted file
217
+ try:
218
+ cache_path.unlink()
219
+ logger.info(f"Removed corrupted token cache: {cache_path}")
220
+ except Exception as cleanup_error:
221
+ logger.warning(f"Could not remove corrupted cache file: {cleanup_error}")
180
222
  return False
181
223
 
182
224
  def _save_token_to_cache(self) -> None:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "meta-ads-mcp"
7
- version = "0.7.6"
7
+ version = "0.7.8"
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"