amazon-ads-mcp 0.2.7__py3-none-any.whl

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 (82) hide show
  1. amazon_ads_mcp/__init__.py +11 -0
  2. amazon_ads_mcp/auth/__init__.py +33 -0
  3. amazon_ads_mcp/auth/base.py +211 -0
  4. amazon_ads_mcp/auth/hooks.py +172 -0
  5. amazon_ads_mcp/auth/manager.py +791 -0
  6. amazon_ads_mcp/auth/oauth_state_store.py +277 -0
  7. amazon_ads_mcp/auth/providers/__init__.py +14 -0
  8. amazon_ads_mcp/auth/providers/direct.py +393 -0
  9. amazon_ads_mcp/auth/providers/example_auth0.py.example +216 -0
  10. amazon_ads_mcp/auth/providers/openbridge.py +512 -0
  11. amazon_ads_mcp/auth/registry.py +146 -0
  12. amazon_ads_mcp/auth/secure_token_store.py +297 -0
  13. amazon_ads_mcp/auth/token_store.py +723 -0
  14. amazon_ads_mcp/config/__init__.py +5 -0
  15. amazon_ads_mcp/config/sampling.py +111 -0
  16. amazon_ads_mcp/config/settings.py +366 -0
  17. amazon_ads_mcp/exceptions.py +314 -0
  18. amazon_ads_mcp/middleware/__init__.py +11 -0
  19. amazon_ads_mcp/middleware/authentication.py +1474 -0
  20. amazon_ads_mcp/middleware/caching.py +177 -0
  21. amazon_ads_mcp/middleware/oauth.py +175 -0
  22. amazon_ads_mcp/middleware/sampling.py +112 -0
  23. amazon_ads_mcp/models/__init__.py +320 -0
  24. amazon_ads_mcp/models/amc_models.py +837 -0
  25. amazon_ads_mcp/models/api_responses.py +847 -0
  26. amazon_ads_mcp/models/base_models.py +215 -0
  27. amazon_ads_mcp/models/builtin_responses.py +496 -0
  28. amazon_ads_mcp/models/dsp_models.py +556 -0
  29. amazon_ads_mcp/models/stores_brands.py +610 -0
  30. amazon_ads_mcp/server/__init__.py +6 -0
  31. amazon_ads_mcp/server/__main__.py +6 -0
  32. amazon_ads_mcp/server/builtin_prompts.py +269 -0
  33. amazon_ads_mcp/server/builtin_tools.py +962 -0
  34. amazon_ads_mcp/server/file_routes.py +547 -0
  35. amazon_ads_mcp/server/html_templates.py +149 -0
  36. amazon_ads_mcp/server/mcp_server.py +327 -0
  37. amazon_ads_mcp/server/openapi_utils.py +158 -0
  38. amazon_ads_mcp/server/sampling_handler.py +251 -0
  39. amazon_ads_mcp/server/server_builder.py +751 -0
  40. amazon_ads_mcp/server/sidecar_loader.py +178 -0
  41. amazon_ads_mcp/server/transform_executor.py +827 -0
  42. amazon_ads_mcp/tools/__init__.py +22 -0
  43. amazon_ads_mcp/tools/cache_management.py +105 -0
  44. amazon_ads_mcp/tools/download_tools.py +267 -0
  45. amazon_ads_mcp/tools/identity.py +236 -0
  46. amazon_ads_mcp/tools/oauth.py +598 -0
  47. amazon_ads_mcp/tools/profile.py +150 -0
  48. amazon_ads_mcp/tools/profile_listing.py +285 -0
  49. amazon_ads_mcp/tools/region.py +320 -0
  50. amazon_ads_mcp/tools/region_identity.py +175 -0
  51. amazon_ads_mcp/utils/__init__.py +6 -0
  52. amazon_ads_mcp/utils/async_compat.py +215 -0
  53. amazon_ads_mcp/utils/errors.py +452 -0
  54. amazon_ads_mcp/utils/export_content_type_resolver.py +249 -0
  55. amazon_ads_mcp/utils/export_download_handler.py +579 -0
  56. amazon_ads_mcp/utils/header_resolver.py +81 -0
  57. amazon_ads_mcp/utils/http/__init__.py +56 -0
  58. amazon_ads_mcp/utils/http/circuit_breaker.py +127 -0
  59. amazon_ads_mcp/utils/http/client_manager.py +329 -0
  60. amazon_ads_mcp/utils/http/request.py +207 -0
  61. amazon_ads_mcp/utils/http/resilience.py +512 -0
  62. amazon_ads_mcp/utils/http/resilient_client.py +195 -0
  63. amazon_ads_mcp/utils/http/retry.py +76 -0
  64. amazon_ads_mcp/utils/http_client.py +873 -0
  65. amazon_ads_mcp/utils/media/__init__.py +21 -0
  66. amazon_ads_mcp/utils/media/negotiator.py +243 -0
  67. amazon_ads_mcp/utils/media/types.py +199 -0
  68. amazon_ads_mcp/utils/openapi/__init__.py +16 -0
  69. amazon_ads_mcp/utils/openapi/json.py +55 -0
  70. amazon_ads_mcp/utils/openapi/loader.py +263 -0
  71. amazon_ads_mcp/utils/openapi/refs.py +46 -0
  72. amazon_ads_mcp/utils/region_config.py +200 -0
  73. amazon_ads_mcp/utils/response_wrapper.py +171 -0
  74. amazon_ads_mcp/utils/sampling_helpers.py +156 -0
  75. amazon_ads_mcp/utils/sampling_wrapper.py +173 -0
  76. amazon_ads_mcp/utils/security.py +630 -0
  77. amazon_ads_mcp/utils/tool_naming.py +137 -0
  78. amazon_ads_mcp-0.2.7.dist-info/METADATA +664 -0
  79. amazon_ads_mcp-0.2.7.dist-info/RECORD +82 -0
  80. amazon_ads_mcp-0.2.7.dist-info/WHEEL +4 -0
  81. amazon_ads_mcp-0.2.7.dist-info/entry_points.txt +3 -0
  82. amazon_ads_mcp-0.2.7.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,150 @@
1
+ """Profile management tools for Amazon Ads MCP.
2
+
3
+ This module provides tools for managing Amazon Ads profiles,
4
+ including setting and getting the active profile ID.
5
+
6
+ The tools handle profile selection for API operations and
7
+ provide fallback mechanisms to default profiles when needed.
8
+
9
+ Key Features:
10
+
11
+ - Set active profile ID for API scope headers
12
+ - Retrieve current active profile with source information
13
+ - Clear profile settings to fall back to defaults
14
+ - Comprehensive error handling and logging
15
+
16
+ Examples:
17
+ >>> result = await set_active_profile("123456789")
18
+ >>> profile_info = await get_active_profile()
19
+ >>> clear_result = await clear_active_profile()
20
+ """
21
+
22
+ import logging
23
+
24
+ from ..auth.manager import get_auth_manager
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ async def set_active_profile(profile_id: str) -> dict:
30
+ """Set the active Amazon Ads profile ID.
31
+
32
+ Sets the profile ID to be used in the Amazon-Advertising-API-Scope
33
+ header for subsequent API calls. This profile ID is associated with
34
+ the current active identity and overrides any default profile.
35
+
36
+ The profile ID will be used for all API requests until cleared or
37
+ changed. This setting is per-identity and does not affect other
38
+ active identities.
39
+
40
+ :param profile_id: The Amazon Ads profile ID to use
41
+ :type profile_id: str
42
+ :return: Success response with profile ID confirmation
43
+ :rtype: dict
44
+ :raises Exception: If setting the active profile fails
45
+
46
+ .. example::
47
+ >>> result = await set_active_profile("123456789")
48
+ >>> print(f"Profile set: {result['profile_id']}")
49
+ """
50
+ try:
51
+ auth_manager = get_auth_manager()
52
+ auth_manager.set_active_profile_id(profile_id)
53
+
54
+ return {
55
+ "success": True,
56
+ "profile_id": profile_id,
57
+ "message": f"Active profile set to {profile_id}",
58
+ }
59
+ except Exception as e:
60
+ logger.error(f"Failed to set active profile: {e}")
61
+ raise
62
+
63
+
64
+ async def get_active_profile() -> dict:
65
+ """Get the currently active Amazon Ads profile ID.
66
+
67
+ Returns the profile ID that will be used for API requests,
68
+ which could be from:
69
+
70
+ 1. Explicitly set profile for current identity
71
+ 2. Default from AMAZON_ADS_PROFILE_ID environment variable
72
+ 3. None if no profile is set
73
+
74
+ The response includes information about the source of the
75
+ profile ID for debugging purposes.
76
+
77
+ The function provides transparency about where the profile ID
78
+ originates from to help with troubleshooting API scope issues.
79
+
80
+ :return: Current profile information with source details
81
+ :rtype: dict
82
+ :raises Exception: If getting the active profile fails
83
+
84
+ .. example::
85
+ >>> profile_info = await get_active_profile()
86
+ >>> print(f"Profile: {profile_info.get('profile_id')}")
87
+ >>> print(f"Source: {profile_info.get('source')}")
88
+ """
89
+ try:
90
+ auth_manager = get_auth_manager()
91
+ profile_id = auth_manager.get_active_profile_id()
92
+
93
+ if profile_id:
94
+ return {
95
+ "success": True,
96
+ "profile_id": profile_id,
97
+ "source": auth_manager.get_profile_source(),
98
+ }
99
+ else:
100
+ return {
101
+ "success": True,
102
+ "profile_id": None,
103
+ "message": "No active profile set",
104
+ }
105
+ except Exception as e:
106
+ logger.error(f"Failed to get active profile: {e}")
107
+ raise
108
+
109
+
110
+ async def clear_active_profile() -> dict:
111
+ """Clear the active profile ID for the current identity.
112
+
113
+ Removes the explicitly set profile ID, falling back to the
114
+ default profile ID from environment if available. This is
115
+ useful for resetting profile selection to system defaults.
116
+
117
+ After clearing, the profile selection falls back to the
118
+ AMAZON_ADS_PROFILE_ID environment variable if set, or None
119
+ if no default is configured.
120
+
121
+ :return: Success response with fallback profile information
122
+ :rtype: dict
123
+ :raises Exception: If clearing the active profile fails
124
+
125
+ .. example::
126
+ >>> result = await clear_active_profile()
127
+ >>> if result.get('fallback_profile_id'):
128
+ ... print(f"Fell back to: {result['fallback_profile_id']}")
129
+ """
130
+ try:
131
+ auth_manager = get_auth_manager()
132
+ auth_manager.clear_active_profile_id()
133
+
134
+ # Check what we're falling back to
135
+ fallback_profile = auth_manager.get_active_profile_id()
136
+
137
+ if fallback_profile:
138
+ return {
139
+ "success": True,
140
+ "message": "Profile cleared",
141
+ "fallback_profile_id": fallback_profile,
142
+ }
143
+ else:
144
+ return {
145
+ "success": True,
146
+ "message": "Profile cleared, no fallback profile available",
147
+ }
148
+ except Exception as e:
149
+ logger.error(f"Failed to clear active profile: {e}")
150
+ raise
@@ -0,0 +1,285 @@
1
+ """Profile listing tools with server-side caching and bounded responses."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ import time
8
+ from dataclasses import dataclass
9
+ from typing import Any, Callable, Dict, List, Optional, Tuple
10
+
11
+ from ..auth.manager import get_auth_manager
12
+ from ..config.settings import Settings
13
+ from ..utils.http import get_http_client
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ DEFAULT_CACHE_TTL_SECONDS = int(os.getenv("AMAZON_ADS_PROFILE_CACHE_TTL", "300"))
19
+ MAX_SEARCH_LIMIT = 50
20
+ DEFAULT_SEARCH_LIMIT = 50
21
+ MAX_PAGE_LIMIT = 100
22
+ DEFAULT_PAGE_LIMIT = 100
23
+ PROFILE_SELECTION_THRESHOLD = 50
24
+
25
+
26
+ @dataclass
27
+ class CacheEntry:
28
+ profiles: List[Dict[str, Any]]
29
+ timestamp: float
30
+
31
+
32
+ class ProfileCache:
33
+ """In-memory profile cache scoped by identity and region."""
34
+
35
+ def __init__(self, ttl_seconds: int = DEFAULT_CACHE_TTL_SECONDS) -> None:
36
+ self._cache: Dict[Tuple[str, str], CacheEntry] = {}
37
+ self._ttl_seconds = ttl_seconds
38
+
39
+ def _is_expired(self, entry: CacheEntry, now: float) -> bool:
40
+ return (now - entry.timestamp) > self._ttl_seconds
41
+
42
+ async def get_profiles(
43
+ self,
44
+ key: Tuple[str, str],
45
+ fetcher: Callable[[], Any],
46
+ force_refresh: bool = False,
47
+ ) -> Tuple[List[Dict[str, Any]], bool]:
48
+ now = time.time()
49
+ entry = self._cache.get(key)
50
+
51
+ if entry and not force_refresh and not self._is_expired(entry, now):
52
+ return entry.profiles, False
53
+
54
+ try:
55
+ profiles = await fetcher()
56
+ self._cache[key] = CacheEntry(profiles=profiles, timestamp=now)
57
+ return profiles, False
58
+ except Exception as exc:
59
+ if entry:
60
+ logger.warning("Using stale profile cache for %s: %s", key, exc)
61
+ return entry.profiles, True
62
+ raise
63
+
64
+ def clear(self, key: Tuple[str, str]) -> None:
65
+ self._cache.pop(key, None)
66
+
67
+ def get_entry(self, key: Tuple[str, str]) -> Optional[CacheEntry]:
68
+ return self._cache.get(key)
69
+
70
+
71
+ _profile_cache = ProfileCache()
72
+
73
+
74
+ def _get_cache_key() -> Tuple[str, str]:
75
+ auth_manager = get_auth_manager()
76
+ identity_id = auth_manager.get_active_identity_id() or "default"
77
+ region = auth_manager.get_active_region() or Settings().amazon_ads_region
78
+ return identity_id, region
79
+
80
+
81
+ def _apply_limit(limit: Optional[int], default: int, max_limit: int) -> int:
82
+ if limit is None:
83
+ return default
84
+ try:
85
+ value = int(limit)
86
+ except (TypeError, ValueError):
87
+ return default
88
+ if value <= 0:
89
+ return default
90
+ return min(value, max_limit)
91
+
92
+
93
+ def _normalize_profile(profile: Dict[str, Any]) -> Dict[str, str]:
94
+ account_info = profile.get("accountInfo") or {}
95
+ return {
96
+ "profile_id": str(profile.get("profileId", "")),
97
+ "name": str(account_info.get("name", "")),
98
+ "country_code": str(profile.get("countryCode", "")),
99
+ "type": str(account_info.get("type", "")),
100
+ }
101
+
102
+
103
+ def _matches_filters(
104
+ profile: Dict[str, Any],
105
+ query: Optional[str],
106
+ country_code: Optional[str],
107
+ account_type: Optional[str],
108
+ ) -> bool:
109
+ if country_code:
110
+ if str(profile.get("countryCode", "")).upper() != country_code.upper():
111
+ return False
112
+
113
+ if account_type:
114
+ p_type = (profile.get("accountInfo") or {}).get("type", "")
115
+ if str(p_type).lower() != account_type.lower():
116
+ return False
117
+
118
+ if query:
119
+ q = query.lower()
120
+ name = (profile.get("accountInfo") or {}).get("name", "")
121
+ profile_id = str(profile.get("profileId", "")).lower()
122
+ if q not in str(name).lower() and q not in profile_id:
123
+ return False
124
+
125
+ return True
126
+
127
+
128
+ async def _fetch_profiles() -> List[Dict[str, Any]]:
129
+ auth_manager = get_auth_manager()
130
+ credentials = await auth_manager.get_active_credentials()
131
+ base_url = credentials.base_url or Settings().region_endpoint
132
+ client = await get_http_client(
133
+ authenticated=True,
134
+ auth_manager=auth_manager,
135
+ base_url=base_url,
136
+ )
137
+ response = await client.get("/v2/profiles")
138
+ response.raise_for_status()
139
+ data = response.json()
140
+ return data if isinstance(data, list) else []
141
+
142
+
143
+ async def _get_profiles_cached(force_refresh: bool = False) -> Tuple[List[Dict[str, Any]], bool]:
144
+ key = _get_cache_key()
145
+ return await _profile_cache.get_profiles(key, _fetch_profiles, force_refresh=force_refresh)
146
+
147
+
148
+ async def get_profiles_cached(
149
+ force_refresh: bool = False,
150
+ ) -> Tuple[List[Dict[str, Any]], bool]:
151
+ """Public wrapper for fetching cached profiles."""
152
+ return await _get_profiles_cached(force_refresh=force_refresh)
153
+
154
+ async def summarize_profiles() -> Dict[str, Any]:
155
+ profiles, stale = await _get_profiles_cached()
156
+
157
+ by_country: Dict[str, int] = {}
158
+ by_type: Dict[str, int] = {}
159
+ for profile in profiles:
160
+ country = str(profile.get("countryCode", ""))
161
+ account_type = str((profile.get("accountInfo") or {}).get("type", ""))
162
+ if country:
163
+ by_country[country] = by_country.get(country, 0) + 1
164
+ if account_type:
165
+ by_type[account_type] = by_type.get(account_type, 0) + 1
166
+
167
+ message = "Ask for a country or advertiser name to narrow results."
168
+ if stale:
169
+ message = "Using cached profile list; data may be stale."
170
+
171
+ return {
172
+ "total_count": len(profiles),
173
+ "by_country": by_country,
174
+ "by_type": by_type,
175
+ "message": message,
176
+ "stale": stale,
177
+ }
178
+
179
+
180
+ async def search_profiles(
181
+ query: Optional[str] = None,
182
+ country_code: Optional[str] = None,
183
+ account_type: Optional[str] = None,
184
+ limit: Optional[int] = None,
185
+ ) -> Dict[str, Any]:
186
+ profiles, stale = await _get_profiles_cached()
187
+ filtered = [
188
+ profile
189
+ for profile in profiles
190
+ if _matches_filters(profile, query, country_code, account_type)
191
+ ]
192
+
193
+ limit_value = _apply_limit(limit, DEFAULT_SEARCH_LIMIT, MAX_SEARCH_LIMIT)
194
+ items = [_normalize_profile(profile) for profile in filtered[:limit_value]]
195
+
196
+ message = None
197
+ if stale:
198
+ message = "Using cached profile list; data may be stale."
199
+
200
+ total_count = len(filtered)
201
+ returned_count = len(items)
202
+ return {
203
+ "items": items,
204
+ "total_count": total_count,
205
+ "returned_count": returned_count,
206
+ "has_more": returned_count < total_count,
207
+ "message": message,
208
+ "stale": stale,
209
+ }
210
+
211
+
212
+ async def page_profiles(
213
+ country_code: Optional[str] = None,
214
+ account_type: Optional[str] = None,
215
+ offset: int = 0,
216
+ limit: Optional[int] = None,
217
+ ) -> Dict[str, Any]:
218
+ profiles, stale = await _get_profiles_cached()
219
+ filtered = [
220
+ profile
221
+ for profile in profiles
222
+ if _matches_filters(profile, None, country_code, account_type)
223
+ ]
224
+
225
+ limit_value = _apply_limit(limit, DEFAULT_PAGE_LIMIT, MAX_PAGE_LIMIT)
226
+ try:
227
+ offset_value = int(offset)
228
+ except (TypeError, ValueError):
229
+ offset_value = 0
230
+ offset_value = max(0, offset_value)
231
+ page = filtered[offset_value : offset_value + limit_value]
232
+ items = [_normalize_profile(profile) for profile in page]
233
+
234
+ message = None
235
+ if stale:
236
+ message = "Using cached profile list; data may be stale."
237
+
238
+ total_count = len(filtered)
239
+ returned_count = len(items)
240
+ has_more = (offset_value + returned_count) < total_count
241
+ next_offset = offset_value + returned_count if has_more else None
242
+
243
+ return {
244
+ "items": items,
245
+ "total_count": total_count,
246
+ "returned_count": returned_count,
247
+ "has_more": has_more,
248
+ "next_offset": next_offset,
249
+ "message": message,
250
+ "stale": stale,
251
+ }
252
+
253
+
254
+ async def refresh_profiles_cache() -> Dict[str, Any]:
255
+ """Force refresh the cached profile list for the current identity and region."""
256
+ key = _get_cache_key()
257
+ try:
258
+ profiles, stale = await _get_profiles_cached(force_refresh=True)
259
+ except Exception as exc:
260
+ return {
261
+ "success": False,
262
+ "total_count": 0,
263
+ "cache_timestamp": None,
264
+ "message": f"Failed to refresh profiles: {exc}",
265
+ "stale": False,
266
+ }
267
+
268
+ entry = _profile_cache.get_entry(key)
269
+ cache_timestamp = entry.timestamp if entry else None
270
+ if stale:
271
+ return {
272
+ "success": False,
273
+ "total_count": len(profiles),
274
+ "cache_timestamp": cache_timestamp,
275
+ "message": "Refresh failed; using cached profile list.",
276
+ "stale": True,
277
+ }
278
+
279
+ return {
280
+ "success": True,
281
+ "total_count": len(profiles),
282
+ "cache_timestamp": cache_timestamp,
283
+ "message": "Profile cache refreshed.",
284
+ "stale": False,
285
+ }