meta-ads-mcp 1.0.0__tar.gz → 1.0.1__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 (79) hide show
  1. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/PKG-INFO +2 -2
  2. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/README.md +1 -1
  3. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/__init__.py +1 -1
  4. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/insights.py +8 -2
  5. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/pyproject.toml +1 -1
  6. meta_ads_mcp-1.0.1/tests/test_insights_pagination.py +257 -0
  7. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/.github/workflows/publish.yml +0 -0
  8. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/.github/workflows/test.yml +0 -0
  9. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/.gitignore +0 -0
  10. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/CUSTOM_META_APP.md +0 -0
  11. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/Dockerfile +0 -0
  12. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/LICENSE +0 -0
  13. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/LOCAL_INSTALLATION.md +0 -0
  14. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/META_API_NOTES.md +0 -0
  15. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/RELEASE.md +0 -0
  16. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/STREAMABLE_HTTP_SETUP.md +0 -0
  17. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/examples/README.md +0 -0
  18. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/examples/example_http_client.py +0 -0
  19. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/future_improvements.md +0 -0
  20. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/images/meta-ads-example.png +0 -0
  21. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_auth.sh +0 -0
  22. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/__main__.py +0 -0
  23. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/__init__.py +0 -0
  24. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/accounts.py +0 -0
  25. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/ads.py +0 -0
  26. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/ads_library.py +0 -0
  27. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/adsets.py +0 -0
  28. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/api.py +0 -0
  29. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/auth.py +0 -0
  30. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/authentication.py +0 -0
  31. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/budget_schedules.py +0 -0
  32. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/callback_server.py +0 -0
  33. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/campaigns.py +0 -0
  34. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/duplication.py +0 -0
  35. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/http_auth_integration.py +0 -0
  36. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/openai_deep_research.py +0 -0
  37. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/pipeboard_auth.py +0 -0
  38. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/reports.py +0 -0
  39. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/resources.py +0 -0
  40. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/server.py +0 -0
  41. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/targeting.py +0 -0
  42. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/utils.py +0 -0
  43. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/requirements.txt +0 -0
  44. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/setup.py +0 -0
  45. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/smithery.yaml +0 -0
  46. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/README.md +0 -0
  47. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/README_REGRESSION_TESTS.md +0 -0
  48. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/__init__.py +0 -0
  49. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/conftest.py +0 -0
  50. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/e2e_account_info_search_issue.py +0 -0
  51. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_account_info_access_fix.py +0 -0
  52. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_account_search.py +0 -0
  53. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_budget_update.py +0 -0
  54. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_budget_update_e2e.py +0 -0
  55. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_dsa_beneficiary.py +0 -0
  56. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_dsa_integration.py +0 -0
  57. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_duplication.py +0 -0
  58. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_duplication_regression.py +0 -0
  59. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_dynamic_creatives.py +0 -0
  60. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_estimate_audience_size.py +0 -0
  61. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_estimate_audience_size_e2e.py +0 -0
  62. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_get_account_pages.py +0 -0
  63. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_get_ad_creatives_fix.py +0 -0
  64. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_get_ad_image_quality_improvements.py +0 -0
  65. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_get_ad_image_regression.py +0 -0
  66. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_http_transport.py +0 -0
  67. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_insights_actions_and_values_e2e.py +0 -0
  68. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_integration_openai_mcp.py +0 -0
  69. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_is_dynamic_creative_adset.py +0 -0
  70. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_mobile_app_adset_creation.py +0 -0
  71. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_mobile_app_adset_issue.py +0 -0
  72. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_openai.py +0 -0
  73. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_openai_mcp_deep_research.py +0 -0
  74. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_page_discovery.py +0 -0
  75. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_page_discovery_integration.py +0 -0
  76. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_targeting.py +0 -0
  77. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_targeting_search_e2e.py +0 -0
  78. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_update_ad_creative_id.py +0 -0
  79. {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_upload_ad_image.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meta-ads-mcp
3
- Version: 1.0.0
3
+ Version: 1.0.1
4
4
  Summary: Model Context Protocol (MCP) server 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
@@ -34,7 +34,7 @@ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server for in
34
34
  ## Community & Support
35
35
 
36
36
  - [Discord](https://discord.gg/YzMwQ8zrjr). Join the community.
37
- - [Email Support](info@pipeboard.co). Email us for support.
37
+ - [Email Support](mailto:info@pipeboard.co). Email us for support.
38
38
 
39
39
  ## Table of Contents
40
40
 
@@ -9,7 +9,7 @@ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server for in
9
9
  ## Community & Support
10
10
 
11
11
  - [Discord](https://discord.gg/YzMwQ8zrjr). Join the community.
12
- - [Email Support](info@pipeboard.co). Email us for support.
12
+ - [Email Support](mailto:info@pipeboard.co). Email us for support.
13
13
 
14
14
  ## Table of Contents
15
15
 
@@ -6,7 +6,7 @@ This package provides a Meta Ads MCP integration
6
6
 
7
7
  from meta_ads_mcp.core.server import main
8
8
 
9
- __version__ = "1.0.0"
9
+ __version__ = "1.0.1"
10
10
 
11
11
  __all__ = [
12
12
  'get_ad_accounts',
@@ -13,7 +13,7 @@ import datetime
13
13
  @meta_api_tool
14
14
  async def get_insights(object_id: str, access_token: Optional[str] = None,
15
15
  time_range: Union[str, Dict[str, str]] = "maximum", breakdown: str = "",
16
- level: str = "ad") -> str:
16
+ level: str = "ad", limit: int = 25, after: str = "") -> str:
17
17
  """
18
18
  Get performance insights for a campaign, ad set, ad or account.
19
19
 
@@ -49,6 +49,8 @@ async def get_insights(object_id: str, access_token: Optional[str] = None,
49
49
  marketing_messages_btn_name, impression_view_time_advertiser_hour_v2, comscore_market,
50
50
  comscore_market_code
51
51
  level: Level of aggregation (ad, adset, campaign, account)
52
+ limit: Maximum number of results to return per page (default: 25, Meta API allows much higher values)
53
+ after: Pagination cursor to get the next set of results. Use the 'after' cursor from previous response's paging.next field.
52
54
  """
53
55
  if not object_id:
54
56
  return json.dumps({"error": "No object ID provided"}, indent=2)
@@ -56,7 +58,8 @@ async def get_insights(object_id: str, access_token: Optional[str] = None,
56
58
  endpoint = f"{object_id}/insights"
57
59
  params = {
58
60
  "fields": "account_id,account_name,campaign_id,campaign_name,adset_id,adset_name,ad_id,ad_name,impressions,clicks,spend,cpc,cpm,ctr,reach,frequency,actions,action_values,conversions,unique_clicks,cost_per_action_type",
59
- "level": level
61
+ "level": level,
62
+ "limit": limit
60
63
  }
61
64
 
62
65
  # Handle time range based on type
@@ -73,6 +76,9 @@ async def get_insights(object_id: str, access_token: Optional[str] = None,
73
76
  if breakdown:
74
77
  params["breakdowns"] = breakdown
75
78
 
79
+ if after:
80
+ params["after"] = after
81
+
76
82
  data = await make_api_request(endpoint, access_token, params)
77
83
 
78
84
  return json.dumps(data, indent=2)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "meta-ads-mcp"
7
- version = "1.0.0"
7
+ version = "1.0.1"
8
8
  description = "Model Context Protocol (MCP) server for interacting with Meta Ads API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1,257 @@
1
+ """Test pagination functionality for insights endpoint."""
2
+
3
+ import pytest
4
+ import json
5
+ from unittest.mock import AsyncMock, patch
6
+ from meta_ads_mcp.core.insights import get_insights
7
+
8
+
9
+ class TestInsightsPagination:
10
+ """Test suite for pagination functionality in get_insights"""
11
+
12
+ @pytest.fixture
13
+ def mock_auth_manager(self):
14
+ """Mock for the authentication manager"""
15
+ with patch('meta_ads_mcp.core.api.auth_manager') as mock, \
16
+ patch('meta_ads_mcp.core.api.get_current_access_token') as mock_get_token:
17
+ # Mock a valid access token
18
+ mock.get_current_access_token.return_value = "test_access_token"
19
+ mock.is_token_valid.return_value = True
20
+ mock.app_id = "test_app_id"
21
+ mock_get_token.return_value = "test_access_token"
22
+ yield mock
23
+
24
+ @pytest.fixture
25
+ def valid_account_id(self):
26
+ return "act_701351919139047"
27
+
28
+ @pytest.fixture
29
+ def mock_paginated_response_page1(self):
30
+ """Mock first page of paginated response"""
31
+ return {
32
+ "data": [
33
+ {
34
+ "campaign_id": "campaign_1",
35
+ "campaign_name": "Test Campaign 1",
36
+ "spend": "100.50",
37
+ "impressions": "1000",
38
+ "clicks": "50"
39
+ },
40
+ {
41
+ "campaign_id": "campaign_2",
42
+ "campaign_name": "Test Campaign 2",
43
+ "spend": "200.75",
44
+ "impressions": "2000",
45
+ "clicks": "100"
46
+ }
47
+ ],
48
+ "paging": {
49
+ "cursors": {
50
+ "before": "before_cursor_1",
51
+ "after": "after_cursor_1"
52
+ },
53
+ "next": "https://graph.facebook.com/v20.0/act_123/insights?after=after_cursor_1&limit=2"
54
+ }
55
+ }
56
+
57
+ @pytest.fixture
58
+ def mock_paginated_response_page2(self):
59
+ """Mock second page of paginated response"""
60
+ return {
61
+ "data": [
62
+ {
63
+ "campaign_id": "campaign_3",
64
+ "campaign_name": "Test Campaign 3",
65
+ "spend": "150.25",
66
+ "impressions": "1500",
67
+ "clicks": "75"
68
+ }
69
+ ],
70
+ "paging": {
71
+ "cursors": {
72
+ "before": "before_cursor_2",
73
+ "after": "after_cursor_2"
74
+ }
75
+ }
76
+ }
77
+
78
+ @pytest.mark.asyncio
79
+ async def test_insights_with_limit_parameter(self, mock_auth_manager, valid_account_id, mock_paginated_response_page1):
80
+ """Test that limit parameter is properly passed to API"""
81
+ with patch('meta_ads_mcp.core.insights.make_api_request', new_callable=AsyncMock) as mock_api_request:
82
+ mock_api_request.return_value = mock_paginated_response_page1
83
+
84
+ result = await get_insights(
85
+ object_id=valid_account_id,
86
+ level="campaign",
87
+ time_range="last_30d",
88
+ limit=2
89
+ )
90
+
91
+ # Verify the API was called with correct parameters
92
+ mock_api_request.assert_called_once()
93
+ call_args = mock_api_request.call_args
94
+
95
+ # Check that limit is included in params
96
+ params = call_args[0][2]
97
+ assert params["limit"] == 2
98
+ assert params["level"] == "campaign"
99
+ assert params["date_preset"] == "last_30d"
100
+
101
+ # Verify the response structure
102
+ result_data = json.loads(result)
103
+ assert "data" in result_data
104
+ assert len(result_data["data"]) == 2
105
+ assert "paging" in result_data
106
+
107
+ @pytest.mark.asyncio
108
+ async def test_insights_with_after_cursor(self, mock_auth_manager, valid_account_id, mock_paginated_response_page2):
109
+ """Test that after cursor is properly passed to API for pagination"""
110
+ with patch('meta_ads_mcp.core.insights.make_api_request', new_callable=AsyncMock) as mock_api_request:
111
+ mock_api_request.return_value = mock_paginated_response_page2
112
+
113
+ after_cursor = "after_cursor_1"
114
+ result = await get_insights(
115
+ object_id=valid_account_id,
116
+ level="campaign",
117
+ time_range="last_30d",
118
+ limit=10,
119
+ after=after_cursor
120
+ )
121
+
122
+ # Verify the API was called with correct parameters
123
+ mock_api_request.assert_called_once()
124
+ call_args = mock_api_request.call_args
125
+
126
+ # Check that after cursor is included in params
127
+ params = call_args[0][2]
128
+ assert params["after"] == after_cursor
129
+ assert params["limit"] == 10
130
+
131
+ # Verify the response structure
132
+ result_data = json.loads(result)
133
+ assert "data" in result_data
134
+ assert len(result_data["data"]) == 1
135
+
136
+ @pytest.mark.asyncio
137
+ async def test_insights_default_limit(self, mock_auth_manager, valid_account_id, mock_paginated_response_page1):
138
+ """Test that default limit is 25 when not specified"""
139
+ with patch('meta_ads_mcp.core.insights.make_api_request', new_callable=AsyncMock) as mock_api_request:
140
+ mock_api_request.return_value = mock_paginated_response_page1
141
+
142
+ result = await get_insights(
143
+ object_id=valid_account_id,
144
+ level="campaign"
145
+ )
146
+
147
+ # Verify the API was called with default limit
148
+ mock_api_request.assert_called_once()
149
+ call_args = mock_api_request.call_args
150
+
151
+ params = call_args[0][2]
152
+ assert params["limit"] == 25 # Default value
153
+
154
+ @pytest.mark.asyncio
155
+ async def test_insights_without_after_cursor(self, mock_auth_manager, valid_account_id, mock_paginated_response_page1):
156
+ """Test that after parameter is not included when empty"""
157
+ with patch('meta_ads_mcp.core.insights.make_api_request', new_callable=AsyncMock) as mock_api_request:
158
+ mock_api_request.return_value = mock_paginated_response_page1
159
+
160
+ result = await get_insights(
161
+ object_id=valid_account_id,
162
+ level="campaign",
163
+ after="" # Empty after cursor
164
+ )
165
+
166
+ # Verify the API was called without after parameter
167
+ mock_api_request.assert_called_once()
168
+ call_args = mock_api_request.call_args
169
+
170
+ params = call_args[0][2]
171
+ assert "after" not in params # Should not be included when empty
172
+
173
+ @pytest.mark.asyncio
174
+ async def test_insights_pagination_with_custom_time_range(self, mock_auth_manager, valid_account_id, mock_paginated_response_page1):
175
+ """Test pagination works with custom time range"""
176
+ with patch('meta_ads_mcp.core.insights.make_api_request', new_callable=AsyncMock) as mock_api_request:
177
+ mock_api_request.return_value = mock_paginated_response_page1
178
+
179
+ custom_time_range = {"since": "2024-01-01", "until": "2024-01-31"}
180
+ result = await get_insights(
181
+ object_id=valid_account_id,
182
+ level="campaign",
183
+ time_range=custom_time_range,
184
+ limit=5,
185
+ after="test_cursor"
186
+ )
187
+
188
+ # Verify the API was called with correct parameters
189
+ mock_api_request.assert_called_once()
190
+ call_args = mock_api_request.call_args
191
+
192
+ params = call_args[0][2]
193
+ assert params["limit"] == 5
194
+ assert params["after"] == "test_cursor"
195
+ assert params["time_range"] == json.dumps(custom_time_range)
196
+
197
+ @pytest.mark.asyncio
198
+ async def test_insights_pagination_with_breakdown(self, mock_auth_manager, valid_account_id, mock_paginated_response_page1):
199
+ """Test pagination works with breakdown parameter"""
200
+ with patch('meta_ads_mcp.core.insights.make_api_request', new_callable=AsyncMock) as mock_api_request:
201
+ mock_api_request.return_value = mock_paginated_response_page1
202
+
203
+ result = await get_insights(
204
+ object_id=valid_account_id,
205
+ level="campaign",
206
+ breakdown="age",
207
+ limit=10,
208
+ after="test_cursor_2"
209
+ )
210
+
211
+ # Verify the API was called with correct parameters
212
+ mock_api_request.assert_called_once()
213
+ call_args = mock_api_request.call_args
214
+
215
+ params = call_args[0][2]
216
+ assert params["limit"] == 10
217
+ assert params["after"] == "test_cursor_2"
218
+ assert params["breakdowns"] == "age"
219
+
220
+ @pytest.mark.asyncio
221
+ async def test_insights_large_limit_value(self, mock_auth_manager, valid_account_id, mock_paginated_response_page1):
222
+ """Test that large limit values are accepted (API will enforce its own limits)"""
223
+ with patch('meta_ads_mcp.core.insights.make_api_request', new_callable=AsyncMock) as mock_api_request:
224
+ mock_api_request.return_value = mock_paginated_response_page1
225
+
226
+ result = await get_insights(
227
+ object_id=valid_account_id,
228
+ level="campaign",
229
+ limit=1000 # Large limit - API will enforce its own max
230
+ )
231
+
232
+ # Verify the API was called with the large limit
233
+ mock_api_request.assert_called_once()
234
+ call_args = mock_api_request.call_args
235
+
236
+ params = call_args[0][2]
237
+ assert params["limit"] == 1000
238
+
239
+ @pytest.mark.asyncio
240
+ async def test_insights_paging_response_structure(self, mock_auth_manager, valid_account_id, mock_paginated_response_page1):
241
+ """Test that paging information is preserved in the response"""
242
+ with patch('meta_ads_mcp.core.insights.make_api_request', new_callable=AsyncMock) as mock_api_request:
243
+ mock_api_request.return_value = mock_paginated_response_page1
244
+
245
+ result = await get_insights(
246
+ object_id=valid_account_id,
247
+ level="campaign",
248
+ limit=2
249
+ )
250
+
251
+ # Verify the response includes paging information
252
+ result_data = json.loads(result)
253
+ assert "data" in result_data
254
+ assert "paging" in result_data
255
+ assert "cursors" in result_data["paging"]
256
+ assert "after" in result_data["paging"]["cursors"]
257
+ assert "next" in result_data["paging"]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes