meta-ads-mcp 0.9.0__tar.gz → 0.9.2__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 (72) hide show
  1. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/PKG-INFO +3 -3
  2. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/README.md +1 -1
  3. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/meta_ads_mcp/__init__.py +1 -1
  4. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/meta_ads_mcp/core/accounts.py +2 -2
  5. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/meta_ads_mcp/core/adsets.py +68 -1
  6. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/meta_ads_mcp/core/server.py +1 -1
  7. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/pyproject.toml +10 -2
  8. meta_ads_mcp-0.9.2/tests/test_mobile_app_adset_creation.py +501 -0
  9. meta_ads_mcp-0.9.2/tests/test_mobile_app_adset_issue.py +315 -0
  10. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/.github/workflows/publish.yml +0 -0
  11. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/.github/workflows/test.yml +0 -0
  12. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/.gitignore +0 -0
  13. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/CUSTOM_META_APP.md +0 -0
  14. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/Dockerfile +0 -0
  15. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/LICENSE +0 -0
  16. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/LOCAL_INSTALLATION.md +0 -0
  17. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/META_API_NOTES.md +0 -0
  18. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/RELEASE.md +0 -0
  19. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/STREAMABLE_HTTP_SETUP.md +0 -0
  20. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/examples/README.md +0 -0
  21. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/examples/example_http_client.py +0 -0
  22. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/future_improvements.md +0 -0
  23. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/images/meta-ads-example.png +0 -0
  24. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/meta_ads_auth.sh +0 -0
  25. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/meta_ads_mcp/__main__.py +0 -0
  26. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/meta_ads_mcp/core/__init__.py +0 -0
  27. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/meta_ads_mcp/core/ads.py +0 -0
  28. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/meta_ads_mcp/core/ads_library.py +0 -0
  29. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/meta_ads_mcp/core/api.py +0 -0
  30. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/meta_ads_mcp/core/auth.py +0 -0
  31. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/meta_ads_mcp/core/authentication.py +0 -0
  32. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/meta_ads_mcp/core/budget_schedules.py +0 -0
  33. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/meta_ads_mcp/core/callback_server.py +0 -0
  34. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/meta_ads_mcp/core/campaigns.py +0 -0
  35. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/meta_ads_mcp/core/duplication.py +0 -0
  36. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/meta_ads_mcp/core/http_auth_integration.py +0 -0
  37. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/meta_ads_mcp/core/insights.py +0 -0
  38. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/meta_ads_mcp/core/openai_deep_research.py +0 -0
  39. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/meta_ads_mcp/core/pipeboard_auth.py +0 -0
  40. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/meta_ads_mcp/core/reports.py +0 -0
  41. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/meta_ads_mcp/core/resources.py +0 -0
  42. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/meta_ads_mcp/core/targeting.py +0 -0
  43. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/meta_ads_mcp/core/utils.py +0 -0
  44. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/requirements.txt +0 -0
  45. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/setup.py +0 -0
  46. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/smithery.yaml +0 -0
  47. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/tests/README.md +0 -0
  48. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/tests/README_REGRESSION_TESTS.md +0 -0
  49. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/tests/__init__.py +0 -0
  50. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/tests/conftest.py +0 -0
  51. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/tests/test_account_search.py +0 -0
  52. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/tests/test_budget_update.py +0 -0
  53. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/tests/test_budget_update_e2e.py +0 -0
  54. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/tests/test_dsa_beneficiary.py +0 -0
  55. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/tests/test_dsa_integration.py +0 -0
  56. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/tests/test_duplication.py +0 -0
  57. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/tests/test_duplication_regression.py +0 -0
  58. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/tests/test_dynamic_creatives.py +0 -0
  59. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/tests/test_get_account_pages.py +0 -0
  60. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/tests/test_get_ad_creatives_fix.py +0 -0
  61. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/tests/test_get_ad_image_quality_improvements.py +0 -0
  62. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/tests/test_get_ad_image_regression.py +0 -0
  63. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/tests/test_http_transport.py +0 -0
  64. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/tests/test_insights_actions_and_values.py +0 -0
  65. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/tests/test_integration_openai_mcp.py +0 -0
  66. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/tests/test_openai.py +0 -0
  67. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/tests/test_openai_mcp_deep_research.py +0 -0
  68. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/tests/test_page_discovery.py +0 -0
  69. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/tests/test_page_discovery_integration.py +0 -0
  70. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/tests/test_targeting.py +0 -0
  71. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/tests/test_targeting_search_e2e.py +0 -0
  72. {meta_ads_mcp-0.9.0 → meta_ads_mcp-0.9.2}/tests/test_update_ad_creative_id.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meta-ads-mcp
3
- Version: 0.9.0
3
+ Version: 0.9.2
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,7 +13,7 @@ Classifier: Operating System :: OS Independent
13
13
  Classifier: Programming Language :: Python :: 3
14
14
  Requires-Python: >=3.10
15
15
  Requires-Dist: httpx>=0.26.0
16
- Requires-Dist: mcp[cli]>=1.10.1
16
+ Requires-Dist: mcp[cli]<=1.12.2,>=1.10.1
17
17
  Requires-Dist: pathlib>=1.0.1
18
18
  Requires-Dist: pillow>=10.0.0
19
19
  Requires-Dist: pytest-asyncio>=1.0.0
@@ -136,7 +136,7 @@ For local installation configuration, authentication options, and advanced techn
136
136
  - Inputs:
137
137
  - `access_token` (optional): Meta API access token (will use cached token if not provided)
138
138
  - `user_id`: Meta user ID or "me" for the current user
139
- - `limit`: Maximum number of accounts to return (default: 10)
139
+ - `limit`: Maximum number of accounts to return (default: 200)
140
140
  - Returns: List of accessible ad accounts with their details
141
141
 
142
142
  2. `mcp_meta_ads_get_account_info`
@@ -111,7 +111,7 @@ For local installation configuration, authentication options, and advanced techn
111
111
  - Inputs:
112
112
  - `access_token` (optional): Meta API access token (will use cached token if not provided)
113
113
  - `user_id`: Meta user ID or "me" for the current user
114
- - `limit`: Maximum number of accounts to return (default: 10)
114
+ - `limit`: Maximum number of accounts to return (default: 200)
115
115
  - Returns: List of accessible ad accounts with their details
116
116
 
117
117
  2. `mcp_meta_ads_get_account_info`
@@ -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.9.0"
10
+ __version__ = "0.9.2"
11
11
 
12
12
  __all__ = [
13
13
  'get_ad_accounts',
@@ -8,14 +8,14 @@ from .server import mcp_server
8
8
 
9
9
  @mcp_server.tool()
10
10
  @meta_api_tool
11
- async def get_ad_accounts(access_token: str = None, user_id: str = "me", limit: int = 10) -> str:
11
+ async def get_ad_accounts(access_token: str = None, user_id: str = "me", limit: int = 200) -> str:
12
12
  """
13
13
  Get ad accounts accessible by a user.
14
14
 
15
15
  Args:
16
16
  access_token: Meta API access token (optional - will use cached token if not provided)
17
17
  user_id: Meta user ID or "me" for the current user
18
- limit: Maximum number of accounts to return (default: 10)
18
+ limit: Maximum number of accounts to return (default: 200)
19
19
  """
20
20
  endpoint = f"{user_id}/adaccounts"
21
21
  params = {
@@ -104,6 +104,8 @@ async def create_adset(
104
104
  start_time: str = None,
105
105
  end_time: str = None,
106
106
  dsa_beneficiary: str = None,
107
+ promoted_object: Dict[str, Any] = None,
108
+ destination_type: str = None,
107
109
  access_token: str = None
108
110
  ) -> str:
109
111
  """
@@ -118,13 +120,18 @@ async def create_adset(
118
120
  lifetime_budget: Lifetime budget in account currency (in cents) as a string
119
121
  targeting: Targeting specifications including age, location, interests, etc.
120
122
  Use targeting_automation.advantage_audience=1 for automatic audience finding
121
- optimization_goal: Conversion optimization goal (e.g., 'LINK_CLICKS', 'REACH', 'CONVERSIONS')
123
+ optimization_goal: Conversion optimization goal (e.g., 'LINK_CLICKS', 'REACH', 'CONVERSIONS', 'APP_INSTALLS')
122
124
  billing_event: How you're charged (e.g., 'IMPRESSIONS', 'LINK_CLICKS')
123
125
  bid_amount: Bid amount in account currency (in cents)
124
126
  bid_strategy: Bid strategy (e.g., 'LOWEST_COST', 'LOWEST_COST_WITH_BID_CAP')
125
127
  start_time: Start time in ISO 8601 format (e.g., '2023-12-01T12:00:00-0800')
126
128
  end_time: End time in ISO 8601 format
127
129
  dsa_beneficiary: DSA beneficiary (person/organization benefiting from ads) for European compliance
130
+ promoted_object: Mobile app configuration for APP_INSTALLS campaigns. Required fields: application_id, object_store_url.
131
+ Optional fields: custom_event_type, pixel_id, page_id.
132
+ Example: {"application_id": "123456789012345", "object_store_url": "https://apps.apple.com/app/id123456789"}
133
+ destination_type: Where users are directed after clicking the ad (e.g., 'APP_STORE', 'DEEPLINK', 'APP_INSTALL').
134
+ Required for mobile app campaigns.
128
135
  access_token: Meta API access token (optional - will use cached token if not provided)
129
136
  """
130
137
  # Check required parameters
@@ -143,6 +150,59 @@ async def create_adset(
143
150
  if not billing_event:
144
151
  return json.dumps({"error": "No billing event provided"}, indent=2)
145
152
 
153
+ # Validate mobile app parameters for APP_INSTALLS campaigns
154
+ if optimization_goal == "APP_INSTALLS":
155
+ if not promoted_object:
156
+ return json.dumps({
157
+ "error": "promoted_object is required for APP_INSTALLS optimization goal",
158
+ "details": "Mobile app campaigns must specify which app is being promoted",
159
+ "required_fields": ["application_id", "object_store_url"]
160
+ }, indent=2)
161
+
162
+ # Validate promoted_object structure
163
+ if not isinstance(promoted_object, dict):
164
+ return json.dumps({
165
+ "error": "promoted_object must be a dictionary",
166
+ "example": {"application_id": "123456789012345", "object_store_url": "https://apps.apple.com/app/id123456789"}
167
+ }, indent=2)
168
+
169
+ # Validate required promoted_object fields
170
+ if "application_id" not in promoted_object:
171
+ return json.dumps({
172
+ "error": "promoted_object missing required field: application_id",
173
+ "details": "application_id is the Facebook app ID for your mobile app"
174
+ }, indent=2)
175
+
176
+ if "object_store_url" not in promoted_object:
177
+ return json.dumps({
178
+ "error": "promoted_object missing required field: object_store_url",
179
+ "details": "object_store_url should be the App Store or Google Play URL for your app"
180
+ }, indent=2)
181
+
182
+ # Validate store URL format
183
+ store_url = promoted_object["object_store_url"]
184
+ valid_store_patterns = [
185
+ "apps.apple.com", # iOS App Store
186
+ "play.google.com", # Google Play Store
187
+ "itunes.apple.com" # Alternative iOS format
188
+ ]
189
+
190
+ if not any(pattern in store_url for pattern in valid_store_patterns):
191
+ return json.dumps({
192
+ "error": "Invalid object_store_url format",
193
+ "details": "URL must be from App Store (apps.apple.com) or Google Play (play.google.com)",
194
+ "provided_url": store_url
195
+ }, indent=2)
196
+
197
+ # Validate destination_type if provided
198
+ if destination_type:
199
+ valid_destination_types = ["APP_STORE", "DEEPLINK", "APP_INSTALL"]
200
+ if destination_type not in valid_destination_types:
201
+ return json.dumps({
202
+ "error": f"Invalid destination_type: {destination_type}",
203
+ "valid_values": valid_destination_types
204
+ }, indent=2)
205
+
146
206
  # Basic targeting is required if not provided
147
207
  if not targeting:
148
208
  targeting = {
@@ -187,6 +247,13 @@ async def create_adset(
187
247
  if dsa_beneficiary:
188
248
  params["dsa_beneficiary"] = dsa_beneficiary
189
249
 
250
+ # Add mobile app parameters if provided
251
+ if promoted_object:
252
+ params["promoted_object"] = json.dumps(promoted_object)
253
+
254
+ if destination_type:
255
+ params["destination_type"] = destination_type
256
+
190
257
  try:
191
258
  data = await make_api_request(endpoint, access_token, params, method="POST")
192
259
  return json.dumps(data, indent=2)
@@ -14,7 +14,7 @@ from .pipeboard_auth import pipeboard_auth_manager
14
14
  import time
15
15
 
16
16
  # Initialize FastMCP server
17
- mcp_server = FastMCP("meta-ads", use_consistent_tool_format=True)
17
+ mcp_server = FastMCP("meta-ads")
18
18
 
19
19
  # Register resource URIs
20
20
  mcp_server.resource(uri="meta-ads://resources")(list_resources)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "meta-ads-mcp"
7
- version = "0.9.0"
7
+ version = "0.9.2"
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"
@@ -20,7 +20,7 @@ classifiers = [
20
20
  ]
21
21
  dependencies = [
22
22
  "httpx>=0.26.0",
23
- "mcp[cli]>=1.10.1",
23
+ "mcp[cli]>=1.10.1,<=1.12.2",
24
24
  "python-dotenv>=1.1.0",
25
25
  "requests>=2.32.3",
26
26
  "Pillow>=10.0.0",
@@ -39,3 +39,11 @@ meta-ads-mcp = "meta_ads_mcp:entrypoint"
39
39
 
40
40
  [tool.hatch.build.targets.wheel]
41
41
  packages = ["meta_ads_mcp"]
42
+
43
+ [tool.pytest.ini_options]
44
+ markers = [
45
+ "e2e: marks tests as end-to-end (requires running MCP server) - excluded from default runs",
46
+ ]
47
+ addopts = "-v --strict-markers -m 'not e2e'"
48
+ testpaths = ["tests"]
49
+ asyncio_mode = "auto"
@@ -0,0 +1,501 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Unit Tests for Mobile App Adset Creation Functionality
4
+
5
+ This test suite validates the mobile app parameters implementation for the
6
+ create_adset function in meta_ads_mcp/core/adsets.py.
7
+
8
+ Test cases cover:
9
+ - Mobile app adset creation success scenarios
10
+ - promoted_object parameter validation and formatting
11
+ - destination_type parameter validation
12
+ - Mobile app specific error handling
13
+ - Cross-platform mobile app support (iOS, Android)
14
+ - Integration with APP_INSTALLS optimization goal
15
+
16
+ Usage:
17
+ uv run python -m pytest tests/test_mobile_app_adset_creation.py -v
18
+
19
+ Related to Issue #008: Missing Mobile App Parameters in create_adset Function
20
+ """
21
+
22
+ import pytest
23
+ import json
24
+ import asyncio
25
+ from unittest.mock import AsyncMock, patch, MagicMock
26
+ from typing import Dict, Any, List
27
+
28
+ # Import the function to test
29
+ from meta_ads_mcp.core.adsets import create_adset
30
+
31
+
32
+ class TestMobileAppAdsetCreation:
33
+ """Test suite for mobile app adset creation functionality"""
34
+
35
+ @pytest.fixture
36
+ def mock_api_request(self):
37
+ """Mock for the make_api_request function"""
38
+ with patch('meta_ads_mcp.core.adsets.make_api_request') as mock:
39
+ mock.return_value = {
40
+ "id": "test_mobile_adset_id",
41
+ "name": "Test Mobile App Adset",
42
+ "optimization_goal": "APP_INSTALLS",
43
+ "promoted_object": {
44
+ "application_id": "123456789012345",
45
+ "object_store_url": "https://apps.apple.com/app/id123456789"
46
+ },
47
+ "destination_type": "APP_STORE"
48
+ }
49
+ yield mock
50
+
51
+ @pytest.fixture
52
+ def mock_auth_manager(self):
53
+ """Mock for the authentication manager"""
54
+ with patch('meta_ads_mcp.core.api.auth_manager') as mock, \
55
+ patch('meta_ads_mcp.core.api.get_current_access_token') as mock_get_token:
56
+ # Mock a valid access token
57
+ mock.get_current_access_token.return_value = "test_access_token"
58
+ mock.is_token_valid.return_value = True
59
+ mock.app_id = "test_app_id"
60
+ mock_get_token.return_value = "test_access_token"
61
+ yield mock
62
+
63
+ @pytest.fixture
64
+ def valid_mobile_app_params(self):
65
+ """Valid mobile app parameters for testing"""
66
+ return {
67
+ "account_id": "act_123456789",
68
+ "campaign_id": "campaign_123456789",
69
+ "name": "Test Mobile App Adset",
70
+ "optimization_goal": "APP_INSTALLS",
71
+ "billing_event": "IMPRESSIONS",
72
+ "targeting": {
73
+ "age_min": 18,
74
+ "age_max": 65,
75
+ "app_install_state": "not_installed",
76
+ "geo_locations": {"countries": ["US"]},
77
+ "user_device": ["Android_Smartphone", "iPhone"],
78
+ "user_os": ["Android", "iOS"]
79
+ }
80
+ }
81
+
82
+ @pytest.fixture
83
+ def ios_promoted_object(self):
84
+ """Valid iOS app promoted object"""
85
+ return {
86
+ "application_id": "123456789012345",
87
+ "object_store_url": "https://apps.apple.com/app/id123456789",
88
+ "custom_event_type": "APP_INSTALL"
89
+ }
90
+
91
+ @pytest.fixture
92
+ def android_promoted_object(self):
93
+ """Valid Android app promoted object"""
94
+ return {
95
+ "application_id": "987654321098765",
96
+ "object_store_url": "https://play.google.com/store/apps/details?id=com.example.app",
97
+ "custom_event_type": "APP_INSTALL"
98
+ }
99
+
100
+ @pytest.fixture
101
+ def promoted_object_with_pixel(self):
102
+ """Promoted object with Facebook pixel for tracking"""
103
+ return {
104
+ "application_id": "123456789012345",
105
+ "object_store_url": "https://apps.apple.com/app/id123456789",
106
+ "custom_event_type": "APP_INSTALL",
107
+ "pixel_id": "pixel_123456789"
108
+ }
109
+
110
+ # Test: Mobile App Adset Creation Success
111
+ @pytest.mark.asyncio
112
+ async def test_mobile_app_adset_creation_success_ios(
113
+ self, mock_api_request, mock_auth_manager, valid_mobile_app_params, ios_promoted_object
114
+ ):
115
+ """Test successful iOS mobile app adset creation"""
116
+
117
+ result = await create_adset(
118
+ **valid_mobile_app_params,
119
+ promoted_object=ios_promoted_object,
120
+ destination_type="APP_STORE"
121
+ )
122
+
123
+ # Parse the result
124
+ result_data = json.loads(result)
125
+
126
+ # Verify the API was called with correct parameters
127
+ mock_api_request.assert_called_once()
128
+ call_args = mock_api_request.call_args
129
+
130
+ # Check endpoint (first argument)
131
+ assert call_args[0][0] == f"{valid_mobile_app_params['account_id']}/adsets"
132
+
133
+ # Check parameters (third argument)
134
+ params = call_args[0][2]
135
+ assert 'promoted_object' in params
136
+ assert 'destination_type' in params
137
+
138
+ # Verify promoted_object is properly JSON-encoded
139
+ promoted_obj_param = json.loads(params['promoted_object']) if isinstance(params['promoted_object'], str) else params['promoted_object']
140
+ assert promoted_obj_param['application_id'] == ios_promoted_object['application_id']
141
+ assert promoted_obj_param['object_store_url'] == ios_promoted_object['object_store_url']
142
+
143
+ # Verify destination_type
144
+ assert params['destination_type'] == "APP_STORE"
145
+
146
+ # Verify response structure
147
+ assert 'id' in result_data
148
+ assert result_data['optimization_goal'] == "APP_INSTALLS"
149
+
150
+ @pytest.mark.asyncio
151
+ async def test_mobile_app_adset_creation_success_android(
152
+ self, mock_api_request, mock_auth_manager, valid_mobile_app_params, android_promoted_object
153
+ ):
154
+ """Test successful Android mobile app adset creation"""
155
+
156
+ result = await create_adset(
157
+ **valid_mobile_app_params,
158
+ promoted_object=android_promoted_object,
159
+ destination_type="APP_STORE"
160
+ )
161
+
162
+ # Parse the result
163
+ result_data = json.loads(result)
164
+
165
+ # Verify the API was called
166
+ mock_api_request.assert_called_once()
167
+ call_args = mock_api_request.call_args
168
+ params = call_args[0][2]
169
+
170
+ # Verify Android-specific promoted_object
171
+ promoted_obj_param = json.loads(params['promoted_object']) if isinstance(params['promoted_object'], str) else params['promoted_object']
172
+ assert promoted_obj_param['application_id'] == android_promoted_object['application_id']
173
+ assert "play.google.com" in promoted_obj_param['object_store_url']
174
+
175
+ # Verify response
176
+ assert 'id' in result_data
177
+
178
+ @pytest.mark.asyncio
179
+ async def test_mobile_app_adset_with_pixel_tracking(
180
+ self, mock_api_request, mock_auth_manager, valid_mobile_app_params, promoted_object_with_pixel
181
+ ):
182
+ """Test mobile app adset creation with Facebook pixel tracking"""
183
+
184
+ result = await create_adset(
185
+ **valid_mobile_app_params,
186
+ promoted_object=promoted_object_with_pixel,
187
+ destination_type="APP_STORE"
188
+ )
189
+
190
+ # Verify pixel_id is included
191
+ call_args = mock_api_request.call_args
192
+ params = call_args[0][2]
193
+ promoted_obj_param = json.loads(params['promoted_object']) if isinstance(params['promoted_object'], str) else params['promoted_object']
194
+
195
+ assert 'pixel_id' in promoted_obj_param
196
+ assert promoted_obj_param['pixel_id'] == promoted_object_with_pixel['pixel_id']
197
+
198
+ # Test: Parameter Validation
199
+ @pytest.mark.asyncio
200
+ async def test_invalid_promoted_object_missing_application_id(
201
+ self, mock_api_request, mock_auth_manager, valid_mobile_app_params
202
+ ):
203
+ """Test validation error for promoted_object missing application_id"""
204
+
205
+ invalid_promoted_object = {
206
+ "object_store_url": "https://apps.apple.com/app/id123456789",
207
+ "custom_event_type": "APP_INSTALL"
208
+ }
209
+
210
+ result = await create_adset(
211
+ **valid_mobile_app_params,
212
+ promoted_object=invalid_promoted_object,
213
+ destination_type="APP_STORE"
214
+ )
215
+
216
+ result_data = json.loads(result)
217
+
218
+ # Should return validation error - check for data wrapper format
219
+ if "data" in result_data:
220
+ error_data = json.loads(result_data["data"])
221
+ assert 'error' in error_data
222
+ assert 'application_id' in error_data['error'].lower()
223
+ else:
224
+ assert 'error' in result_data
225
+ assert 'application_id' in result_data['error'].lower()
226
+
227
+ @pytest.mark.asyncio
228
+ async def test_invalid_promoted_object_missing_store_url(
229
+ self, mock_api_request, mock_auth_manager, valid_mobile_app_params
230
+ ):
231
+ """Test validation error for promoted_object missing object_store_url"""
232
+
233
+ invalid_promoted_object = {
234
+ "application_id": "123456789012345",
235
+ "custom_event_type": "APP_INSTALL"
236
+ }
237
+
238
+ result = await create_adset(
239
+ **valid_mobile_app_params,
240
+ promoted_object=invalid_promoted_object,
241
+ destination_type="APP_STORE"
242
+ )
243
+
244
+ result_data = json.loads(result)
245
+
246
+ # Should return validation error - check for data wrapper format
247
+ if "data" in result_data:
248
+ error_data = json.loads(result_data["data"])
249
+ assert 'error' in error_data
250
+ assert 'object_store_url' in error_data['error'].lower()
251
+ else:
252
+ assert 'error' in result_data
253
+ assert 'object_store_url' in result_data['error'].lower()
254
+
255
+ @pytest.mark.asyncio
256
+ async def test_invalid_destination_type(
257
+ self, mock_api_request, mock_auth_manager, valid_mobile_app_params, ios_promoted_object
258
+ ):
259
+ """Test validation error for invalid destination_type value"""
260
+
261
+ result = await create_adset(
262
+ **valid_mobile_app_params,
263
+ promoted_object=ios_promoted_object,
264
+ destination_type="INVALID_TYPE"
265
+ )
266
+
267
+ result_data = json.loads(result)
268
+
269
+ # Should return validation error - check for data wrapper format
270
+ if "data" in result_data:
271
+ error_data = json.loads(result_data["data"])
272
+ assert 'error' in error_data
273
+ assert 'destination_type' in error_data['error'].lower()
274
+ else:
275
+ assert 'error' in result_data
276
+ assert 'destination_type' in result_data['error'].lower()
277
+
278
+ @pytest.mark.asyncio
279
+ async def test_app_installs_requires_promoted_object(
280
+ self, mock_api_request, mock_auth_manager, valid_mobile_app_params
281
+ ):
282
+ """Test that APP_INSTALLS optimization goal requires promoted_object"""
283
+
284
+ result = await create_adset(
285
+ **valid_mobile_app_params,
286
+ # Missing promoted_object
287
+ destination_type="APP_STORE"
288
+ )
289
+
290
+ result_data = json.loads(result)
291
+
292
+ # Should return validation error - check for data wrapper format
293
+ if "data" in result_data:
294
+ error_data = json.loads(result_data["data"])
295
+ assert 'error' in error_data
296
+ assert 'promoted_object' in error_data['error'].lower()
297
+ assert 'app_installs' in error_data['error'].lower()
298
+ else:
299
+ assert 'error' in result_data
300
+ assert 'promoted_object' in result_data['error'].lower()
301
+ assert 'app_installs' in result_data['error'].lower()
302
+
303
+ # Test: Cross-platform Support
304
+ @pytest.mark.asyncio
305
+ async def test_ios_app_store_url_validation(
306
+ self, mock_api_request, mock_auth_manager, valid_mobile_app_params
307
+ ):
308
+ """Test iOS App Store URL format validation"""
309
+
310
+ ios_promoted_object = {
311
+ "application_id": "123456789012345",
312
+ "object_store_url": "https://apps.apple.com/app/id123456789",
313
+ "custom_event_type": "APP_INSTALL"
314
+ }
315
+
316
+ result = await create_adset(
317
+ **valid_mobile_app_params,
318
+ promoted_object=ios_promoted_object,
319
+ destination_type="APP_STORE"
320
+ )
321
+
322
+ result_data = json.loads(result)
323
+
324
+ # Should succeed for valid iOS URL
325
+ assert 'error' not in result_data or result_data.get('error') is None
326
+
327
+ @pytest.mark.asyncio
328
+ async def test_google_play_url_validation(
329
+ self, mock_api_request, mock_auth_manager, valid_mobile_app_params
330
+ ):
331
+ """Test Google Play Store URL format validation"""
332
+
333
+ android_promoted_object = {
334
+ "application_id": "987654321098765",
335
+ "object_store_url": "https://play.google.com/store/apps/details?id=com.example.app",
336
+ "custom_event_type": "APP_INSTALL"
337
+ }
338
+
339
+ result = await create_adset(
340
+ **valid_mobile_app_params,
341
+ promoted_object=android_promoted_object,
342
+ destination_type="APP_STORE"
343
+ )
344
+
345
+ result_data = json.loads(result)
346
+
347
+ # Should succeed for valid Google Play URL
348
+ assert 'error' not in result_data or result_data.get('error') is None
349
+
350
+ @pytest.mark.asyncio
351
+ async def test_invalid_store_url_format(
352
+ self, mock_api_request, mock_auth_manager, valid_mobile_app_params
353
+ ):
354
+ """Test validation error for invalid app store URL format"""
355
+
356
+ invalid_promoted_object = {
357
+ "application_id": "123456789012345",
358
+ "object_store_url": "https://example.com/invalid-url",
359
+ "custom_event_type": "APP_INSTALL"
360
+ }
361
+
362
+ result = await create_adset(
363
+ **valid_mobile_app_params,
364
+ promoted_object=invalid_promoted_object,
365
+ destination_type="APP_STORE"
366
+ )
367
+
368
+ result_data = json.loads(result)
369
+
370
+ # Should return validation error for invalid URL - check for data wrapper format
371
+ if "data" in result_data:
372
+ error_data = json.loads(result_data["data"])
373
+ assert 'error' in error_data
374
+ assert 'store url' in error_data['error'].lower() or 'object_store_url' in error_data['error'].lower()
375
+ else:
376
+ assert 'error' in result_data
377
+ assert 'store url' in result_data['error'].lower() or 'object_store_url' in result_data['error'].lower()
378
+
379
+ # Test: Destination Type Variations
380
+ @pytest.mark.asyncio
381
+ async def test_deeplink_destination_type(
382
+ self, mock_api_request, mock_auth_manager, valid_mobile_app_params, ios_promoted_object
383
+ ):
384
+ """Test DEEPLINK destination_type"""
385
+
386
+ result = await create_adset(
387
+ **valid_mobile_app_params,
388
+ promoted_object=ios_promoted_object,
389
+ destination_type="DEEPLINK"
390
+ )
391
+
392
+ call_args = mock_api_request.call_args
393
+ params = call_args[0][2]
394
+ assert params['destination_type'] == "DEEPLINK"
395
+
396
+ @pytest.mark.asyncio
397
+ async def test_app_install_destination_type(
398
+ self, mock_api_request, mock_auth_manager, valid_mobile_app_params, ios_promoted_object
399
+ ):
400
+ """Test APP_INSTALL destination_type"""
401
+
402
+ result = await create_adset(
403
+ **valid_mobile_app_params,
404
+ promoted_object=ios_promoted_object,
405
+ destination_type="APP_INSTALL"
406
+ )
407
+
408
+ call_args = mock_api_request.call_args
409
+ params = call_args[0][2]
410
+ assert params['destination_type'] == "APP_INSTALL"
411
+
412
+ # Test: Error Handling
413
+ @pytest.mark.asyncio
414
+ async def test_meta_api_error_handling(
415
+ self, mock_auth_manager, valid_mobile_app_params, ios_promoted_object
416
+ ):
417
+ """Test handling of Meta API errors for mobile app adsets"""
418
+
419
+ with patch('meta_ads_mcp.core.adsets.make_api_request') as mock_api:
420
+ # Mock Meta API error response
421
+ mock_api.side_effect = Exception("HTTP Error: 400 - Select a dataset and conversion event for your ad set")
422
+
423
+ result = await create_adset(
424
+ **valid_mobile_app_params,
425
+ promoted_object=ios_promoted_object,
426
+ destination_type="APP_STORE"
427
+ )
428
+
429
+ result_data = json.loads(result)
430
+
431
+ # Should handle the error gracefully - check for data wrapper format
432
+ if "data" in result_data:
433
+ error_data = json.loads(result_data["data"])
434
+ assert 'error' in error_data
435
+ # Check for error text in either error message or details
436
+ error_text = error_data.get('error', '').lower()
437
+ details_text = error_data.get('details', '').lower()
438
+ assert 'dataset' in error_text or 'conversion event' in error_text or \
439
+ 'dataset' in details_text or 'conversion event' in details_text
440
+ else:
441
+ assert 'error' in result_data
442
+ error_text = result_data.get('error', '').lower()
443
+ details_text = result_data.get('details', '').lower()
444
+ assert 'dataset' in error_text or 'conversion event' in error_text or \
445
+ 'dataset' in details_text or 'conversion event' in details_text
446
+
447
+ # Test: Backward Compatibility
448
+ @pytest.mark.asyncio
449
+ async def test_backward_compatibility_non_mobile_campaigns(
450
+ self, mock_api_request, mock_auth_manager
451
+ ):
452
+ """Test that non-mobile campaigns still work without mobile app parameters"""
453
+
454
+ non_mobile_params = {
455
+ "account_id": "act_123456789",
456
+ "campaign_id": "campaign_123456789",
457
+ "name": "Test Web Adset",
458
+ "optimization_goal": "LINK_CLICKS",
459
+ "billing_event": "LINK_CLICKS",
460
+ "targeting": {
461
+ "age_min": 18,
462
+ "age_max": 65,
463
+ "geo_locations": {"countries": ["US"]}
464
+ }
465
+ }
466
+
467
+ result = await create_adset(**non_mobile_params)
468
+
469
+ # Should work without mobile app parameters
470
+ mock_api_request.assert_called_once()
471
+ call_args = mock_api_request.call_args
472
+ params = call_args[0][2]
473
+
474
+ # Should not include mobile app parameters
475
+ assert 'promoted_object' not in params
476
+ assert 'destination_type' not in params
477
+
478
+ @pytest.mark.asyncio
479
+ async def test_optional_mobile_parameters(
480
+ self, mock_api_request, mock_auth_manager, valid_mobile_app_params, ios_promoted_object
481
+ ):
482
+ """Test that mobile app parameters are optional for non-APP_INSTALLS campaigns"""
483
+
484
+ non_app_install_params = valid_mobile_app_params.copy()
485
+ non_app_install_params['optimization_goal'] = "REACH"
486
+
487
+ result = await create_adset(
488
+ **non_app_install_params,
489
+ # Mobile app parameters should be optional for non-APP_INSTALLS
490
+ promoted_object=ios_promoted_object,
491
+ destination_type="APP_STORE"
492
+ )
493
+
494
+ # Should work and include mobile parameters if provided
495
+ mock_api_request.assert_called_once()
496
+ call_args = mock_api_request.call_args
497
+ params = call_args[0][2]
498
+
499
+ # Mobile parameters should be included if provided
500
+ assert 'promoted_object' in params
501
+ assert 'destination_type' in params
@@ -0,0 +1,315 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ E2E Test for mobile app adset creation issue (Issue #008)
4
+
5
+ This test validates that the create_adset tool supports required parameters
6
+ for mobile app campaigns:
7
+ - promoted_object configuration
8
+ - destination_type settings
9
+ - Conversion event dataset linking
10
+ - Custom event type specification
11
+
12
+ Expected Meta API error when parameters are missing:
13
+ "Select a dataset and conversion event for your ad set (Code 100)"
14
+
15
+ Usage (Manual execution only):
16
+ 1. Start the server: uv run python -m meta_ads_mcp --transport streamable-http --port 8080
17
+ 2. Run test: uv run python tests/test_mobile_app_adset_issue.py
18
+
19
+ Or with pytest (explicit E2E flag required):
20
+ uv run python -m pytest tests/test_mobile_app_adset_issue.py -v -m e2e
21
+
22
+ Note: This test is marked as E2E and will NOT run automatically in CI.
23
+ It must be executed manually to validate mobile app campaign functionality.
24
+ """
25
+
26
+ import pytest
27
+ import requests
28
+ import json
29
+ import time
30
+ import sys
31
+ import os
32
+ from typing import Dict, Any
33
+
34
+ # Add project root to path for imports
35
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
36
+
37
+ class MobileAppAdsetTester:
38
+ """Test suite for mobile app adset creation functionality"""
39
+
40
+ def __init__(self, base_url: str = "http://localhost:8080"):
41
+ self.base_url = base_url.rstrip('/')
42
+ self.endpoint = f"{self.base_url}/mcp/"
43
+ self.request_id = 1
44
+
45
+ def _make_request(self, method: str, params: Dict[str, Any] = None,
46
+ headers: Dict[str, str] = None) -> Dict[str, Any]:
47
+ """Make a JSON-RPC request to the MCP server"""
48
+
49
+ # Default headers for MCP protocol with streamable HTTP transport
50
+ default_headers = {
51
+ "Content-Type": "application/json",
52
+ "Accept": "application/json, text/event-stream",
53
+ "User-Agent": "MobileApp-Test-Client/1.0"
54
+ }
55
+
56
+ if headers:
57
+ default_headers.update(headers)
58
+
59
+ payload = {
60
+ "jsonrpc": "2.0",
61
+ "method": method,
62
+ "id": self.request_id
63
+ }
64
+
65
+ if params:
66
+ payload["params"] = params
67
+
68
+ try:
69
+ response = requests.post(
70
+ self.endpoint,
71
+ headers=default_headers,
72
+ json=payload,
73
+ timeout=30 # Increased timeout for API calls
74
+ )
75
+
76
+ self.request_id += 1
77
+
78
+ return {
79
+ "status_code": response.status_code,
80
+ "headers": dict(response.headers),
81
+ "json": response.json() if response.status_code == 200 else None,
82
+ "text": response.text,
83
+ "success": response.status_code == 200
84
+ }
85
+
86
+ except requests.exceptions.RequestException as e:
87
+ return {
88
+ "status_code": 0,
89
+ "headers": {},
90
+ "json": None,
91
+ "text": str(e),
92
+ "success": False,
93
+ "error": str(e)
94
+ }
95
+
96
+ def test_create_adset_tool_exists(self) -> Dict[str, Any]:
97
+ """Test that create_adset tool exists and check its parameters"""
98
+ result = self._make_request("tools/list", {})
99
+
100
+ if not result["success"]:
101
+ return {"success": False, "error": "Failed to get tools list"}
102
+
103
+ tools = result["json"]["result"].get("tools", [])
104
+ create_adset_tool = next((tool for tool in tools if tool["name"] == "create_adset"), None)
105
+
106
+ if not create_adset_tool:
107
+ return {"success": False, "error": "create_adset tool not found"}
108
+
109
+ # Check if mobile app specific parameters are supported
110
+ input_schema = create_adset_tool.get("inputSchema", {})
111
+ properties = input_schema.get("properties", {})
112
+
113
+ mobile_app_params = ["promoted_object", "destination_type"]
114
+ missing_params = []
115
+
116
+ for param in mobile_app_params:
117
+ if param not in properties:
118
+ missing_params.append(param)
119
+
120
+ return {
121
+ "success": True,
122
+ "tool": create_adset_tool,
123
+ "missing_mobile_app_params": missing_params,
124
+ "has_mobile_app_support": len(missing_params) == 0
125
+ }
126
+
127
+ def test_reproduce_mobile_app_error(self) -> Dict[str, Any]:
128
+ """Reproduce mobile app adset creation error scenario"""
129
+
130
+ # Test parameters for mobile app campaign
131
+ test_params = {
132
+ "name": "create_adset",
133
+ "arguments": {
134
+ "account_id": "act_123456789012345", # Generic test account
135
+ "campaign_id": "120230566078340163", # This will likely be invalid but that's OK for testing
136
+ "name": "test mobile app ad set",
137
+ "status": "PAUSED",
138
+ "targeting": {
139
+ "age_max": 65,
140
+ "age_min": 18,
141
+ "app_install_state": "not_installed",
142
+ "geo_locations": {
143
+ "countries": ["DE"],
144
+ "location_types": ["home", "recent"]
145
+ },
146
+ "user_device": ["Android_Smartphone", "Android_Tablet"],
147
+ "user_os": ["Android"],
148
+ "brand_safety_content_filter_levels": ["FACEBOOK_STANDARD", "AN_STANDARD"],
149
+ "targeting_automation": {"advantage_audience": 1}
150
+ },
151
+ "optimization_goal": "APP_INSTALLS",
152
+ "billing_event": "IMPRESSIONS"
153
+ }
154
+ }
155
+
156
+ result = self._make_request("tools/call", test_params)
157
+
158
+ if not result["success"]:
159
+ return {
160
+ "success": False,
161
+ "error": f"MCP call failed: {result.get('text', 'Unknown error')}"
162
+ }
163
+
164
+ # Parse the response
165
+ response_data = result["json"]["result"]
166
+ content = response_data.get("content", [{}])[0].get("text", "")
167
+
168
+ try:
169
+ parsed_content = json.loads(content)
170
+
171
+ # Check if this is an error response
172
+ if "error" in parsed_content:
173
+ error_details = parsed_content["error"]
174
+ if isinstance(error_details, dict) and "details" in error_details:
175
+ meta_error = error_details["details"]
176
+
177
+ # Check for the specific error we're looking for
178
+ if isinstance(meta_error, dict) and "error" in meta_error:
179
+ error_code = meta_error["error"].get("code")
180
+ error_message = meta_error["error"].get("error_user_msg", "")
181
+
182
+ is_dataset_error = (
183
+ error_code == 100 and
184
+ "conversion event" in error_message.lower()
185
+ )
186
+
187
+ return {
188
+ "success": True,
189
+ "reproduced_error": is_dataset_error,
190
+ "error_code": error_code,
191
+ "error_message": error_message,
192
+ "full_response": parsed_content
193
+ }
194
+
195
+ return {
196
+ "success": True,
197
+ "reproduced_error": False,
198
+ "unexpected_response": parsed_content
199
+ }
200
+
201
+ except json.JSONDecodeError as e:
202
+ return {
203
+ "success": False,
204
+ "error": f"Failed to parse response: {e}",
205
+ "raw_content": content
206
+ }
207
+
208
+ # Pytest E2E test class - marked to prevent automatic execution
209
+ @pytest.mark.e2e
210
+ @pytest.mark.skip(reason="E2E test - requires running MCP server - execute manually only")
211
+ class TestMobileAppAdsetIssueE2E:
212
+ """E2E test for mobile app adset creation functionality (Issue #008)"""
213
+
214
+ def setup_method(self):
215
+ """Set up test instance"""
216
+ self.tester = MobileAppAdsetTester()
217
+
218
+ def test_create_adset_tool_has_mobile_app_params(self):
219
+ """Test that create_adset tool exists and has mobile app parameters"""
220
+ result = self.tester.test_create_adset_tool_exists()
221
+
222
+ assert result["success"], f"Tool test failed: {result.get('error', 'Unknown error')}"
223
+
224
+ missing_params = result["missing_mobile_app_params"]
225
+ has_mobile_support = result["has_mobile_app_support"]
226
+
227
+ # Report results but don't fail if parameters are missing (this is what we're testing)
228
+ if missing_params:
229
+ pytest.skip(f"Missing mobile app parameters: {missing_params}")
230
+ else:
231
+ # Parameters are present - mobile app support is available
232
+ assert has_mobile_support, "Tool should have mobile app support when parameters are present"
233
+
234
+ def test_reproduce_mobile_app_error_scenario(self):
235
+ """Test reproducing mobile app adset creation error scenario"""
236
+ result = self.tester.test_reproduce_mobile_app_error()
237
+
238
+ assert result["success"], f"Error reproduction test failed: {result.get('error', 'Unknown error')}"
239
+
240
+ # This test is mainly for validation, not assertion
241
+ # The actual error depends on authentication and server state
242
+ if result.get("reproduced_error"):
243
+ print(f"Reproduced error - Code: {result.get('error_code')}, Message: {result.get('error_message')}")
244
+ else:
245
+ print("Different response received (may indicate parameters are working or auth issues)")
246
+
247
+
248
+ def main():
249
+ """Run mobile app adset creation tests (manual execution)"""
250
+ print("🚀 Mobile App Adset Creation E2E Test")
251
+ print("=" * 50)
252
+ print("⚠️ This is an E2E test - requires MCP server running on localhost:8080")
253
+ print(" Start server with: uv run python -m meta_ads_mcp --transport streamable-http --port 8080")
254
+ print()
255
+
256
+ tester = MobileAppAdsetTester()
257
+
258
+ # Test 1: Check if create_adset tool exists and has mobile app parameters
259
+ print("\n🧪 Test 1: Checking create_adset tool parameters...")
260
+ tool_test = tester.test_create_adset_tool_exists()
261
+
262
+ if tool_test["success"]:
263
+ missing_params = tool_test["missing_mobile_app_params"]
264
+ if missing_params:
265
+ print(f"❌ Missing mobile app parameters: {missing_params}")
266
+ print("⚠️ Mobile app campaigns may not work without these parameters")
267
+ else:
268
+ print("✅ All mobile app parameters are present")
269
+ else:
270
+ print(f"❌ Tool test failed: {tool_test['error']}")
271
+
272
+ # Test 2: Try to reproduce mobile app error scenario
273
+ print("\n🧪 Test 2: Testing mobile app campaign creation...")
274
+ error_test = tester.test_reproduce_mobile_app_error()
275
+
276
+ if error_test["success"]:
277
+ if error_test.get("reproduced_error"):
278
+ print("✅ Successfully reproduced the error!")
279
+ print(f" Error Code: {error_test['error_code']}")
280
+ print(f" Error Message: {error_test['error_message']}")
281
+ else:
282
+ print("⚠️ Error not reproduced - different response received")
283
+ if "unexpected_response" in error_test:
284
+ print(f" Response: {json.dumps(error_test['unexpected_response'], indent=2)}")
285
+ else:
286
+ print(f"❌ Error reproduction test failed: {error_test['error']}")
287
+
288
+ # Summary
289
+ print("\n🏁 TEST SUMMARY")
290
+ print("=" * 30)
291
+
292
+ if tool_test["success"]:
293
+ missing_params = tool_test["missing_mobile_app_params"]
294
+ issue_confirmed = len(missing_params) > 0
295
+ fix_validated = len(missing_params) == 0
296
+
297
+ if fix_validated:
298
+ print("✅ MOBILE APP SUPPORT VALIDATED")
299
+ print(" All required mobile app parameters are present")
300
+ print(" Mobile app campaigns should work correctly!")
301
+ elif issue_confirmed:
302
+ print("❌ MOBILE APP SUPPORT INCOMPLETE")
303
+ print(f" Missing parameters: {missing_params}")
304
+ print(" Mobile app campaigns may fail without these parameters")
305
+ else:
306
+ print("❓ STATUS UNCLEAR")
307
+ print(" Could not determine mobile app parameter status")
308
+ else:
309
+ print("❌ TEST FAILED")
310
+ print(" Could not connect to MCP server or validate tools")
311
+
312
+ return 0
313
+
314
+ if __name__ == "__main__":
315
+ sys.exit(main())
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes