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,320 @@
1
+ """Manage Amazon Ads API region for the MCP server.
2
+
3
+ Provide utilities to set, inspect, and list region information including
4
+ endpoints and sandbox state.
5
+
6
+ Examples
7
+ --------
8
+ .. code-block:: python
9
+
10
+ import asyncio
11
+ from amazon_ads_mcp.tools.region import set_active_region, get_active_region
12
+
13
+ async def main():
14
+ await set_active_region("na")
15
+ info = await get_active_region()
16
+ print(info["region"]) # "na"
17
+
18
+ asyncio.run(main())
19
+ """
20
+
21
+ import logging
22
+ from typing import Literal
23
+
24
+ from ..auth.manager import get_auth_manager
25
+ from ..config.settings import Settings
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ async def set_active_region(region: Literal["na", "eu", "fe"]) -> dict:
31
+ """Set the active Amazon Ads API region.
32
+
33
+ Update API and OAuth endpoints for subsequent calls. When the provider
34
+ binds region to identity, advise switching identities instead.
35
+
36
+ :param region: Target region ("na", "eu", or "fe").
37
+ :return: Result payload with region info and endpoints.
38
+ :raises ValueError: If the region value is invalid.
39
+ :raises Exception: If updating the region fails.
40
+ """
41
+ try:
42
+ # Validate region
43
+ if region not in ["na", "eu", "fe"]:
44
+ raise ValueError(f"Invalid region: {region}. Must be 'na', 'eu', or 'fe'")
45
+
46
+ # Get auth manager to check provider capabilities
47
+ auth_manager = get_auth_manager()
48
+
49
+ # Check if provider controls region through identity
50
+ if (
51
+ hasattr(auth_manager.provider, "region_controlled_by_identity")
52
+ and auth_manager.provider.region_controlled_by_identity()
53
+ ):
54
+ # For providers where region is identity-controlled, we can't directly set the region
55
+ # The region is determined by the active identity
56
+ current_identity = auth_manager.get_active_identity()
57
+ if current_identity:
58
+ identity_region = current_identity.attributes.get("region", "na")
59
+ if identity_region != region:
60
+ return {
61
+ "success": False,
62
+ "error": "REGION_MISMATCH",
63
+ "message": f"Cannot set region to '{region}' when using {auth_manager.provider.provider_type} authentication. "
64
+ f"Current identity is in '{identity_region}' region. "
65
+ f"Please select an identity in the '{region}' region instead.",
66
+ "current_identity": current_identity.attributes.get(
67
+ "name", current_identity.id
68
+ ),
69
+ "identity_region": identity_region,
70
+ "requested_region": region,
71
+ }
72
+ else:
73
+ # Region already matches the identity's region
74
+ return {
75
+ "success": True,
76
+ "message": f"Region is already set to '{region}' via OpenBridge identity",
77
+ "region": region,
78
+ "identity": current_identity.attributes.get(
79
+ "name", current_identity.id
80
+ ),
81
+ }
82
+ else:
83
+ return {
84
+ "success": False,
85
+ "error": "NO_IDENTITY",
86
+ "message": "No active identity set. Please select an identity first using 'set_active_identity'",
87
+ "requested_region": region,
88
+ }
89
+
90
+ # For non-OpenBridge providers (like DirectAmazonAdsProvider)
91
+ # Get the current region first
92
+ old_region = (
93
+ auth_manager.provider.region
94
+ if hasattr(auth_manager.provider, "region")
95
+ else "na"
96
+ )
97
+
98
+ # If using direct auth, update the provider's region
99
+ if hasattr(auth_manager.provider, "_region"):
100
+ # For DirectAmazonAdsProvider - it has a settable _region attribute
101
+ auth_manager.provider._region = region
102
+ # Note: OAuth endpoint is determined dynamically via get_oauth_endpoint()
103
+ # so no need to update it explicitly
104
+
105
+ # Clear cached tokens as they might be region-specific
106
+ if hasattr(auth_manager.provider, "_access_token"):
107
+ auth_manager.provider._access_token = None
108
+ logger.info("Cleared cached access token due to region change")
109
+
110
+ # Map region to name for clarity
111
+ region_names = {
112
+ "na": "North America",
113
+ "eu": "Europe",
114
+ "fe": "Far East",
115
+ }
116
+
117
+ # Get the new endpoint URLs from provider if available
118
+ if hasattr(auth_manager.provider, "get_region_endpoint"):
119
+ region_endpoint = auth_manager.provider.get_region_endpoint(region)
120
+ else:
121
+ # Fallback to settings for display
122
+ settings = Settings()
123
+ region_endpoint = settings.region_endpoint
124
+
125
+ # Get OAuth endpoint if available
126
+ oauth_endpoint = None
127
+ if hasattr(auth_manager.provider, "get_oauth_endpoint"):
128
+ oauth_endpoint = auth_manager.provider.get_oauth_endpoint(region)
129
+
130
+ # Build response
131
+ response = {
132
+ "success": True,
133
+ "previous_region": old_region,
134
+ "new_region": region,
135
+ "region_name": region_names[region],
136
+ "api_endpoint": region_endpoint,
137
+ "message": f"Region changed from {old_region} to {region}",
138
+ }
139
+
140
+ # Add OAuth endpoint if available
141
+ if oauth_endpoint:
142
+ response["oauth_endpoint"] = oauth_endpoint
143
+
144
+ logger.info(
145
+ f"Region changed from {old_region} to {region} ({region_names[region]})"
146
+ )
147
+ return response
148
+
149
+ except Exception as e:
150
+ logger.error(f"Failed to set active region: {e}")
151
+ raise
152
+
153
+
154
+ async def get_active_region() -> dict:
155
+ """Return information about the active region.
156
+
157
+ Include endpoints, sandbox mode, active auth method, and whether the
158
+ region source is identity or configuration.
159
+
160
+ :return: Region information with endpoints and metadata.
161
+ :raises Exception: If retrieval fails.
162
+ """
163
+ try:
164
+ # Get auth manager to access current region
165
+ auth_manager = get_auth_manager()
166
+
167
+ # Get region from provider if available, otherwise use default
168
+ if hasattr(auth_manager.provider, "region"):
169
+ region = auth_manager.provider.region
170
+ else:
171
+ # Fallback to environment/default if provider doesn't have region
172
+ settings = Settings()
173
+ region = settings.amazon_ads_region
174
+
175
+ # Map region to name
176
+ region_names = {
177
+ "na": "North America",
178
+ "eu": "Europe",
179
+ "fe": "Far East",
180
+ }
181
+
182
+ # Get endpoint URLs from provider if available, otherwise from settings
183
+ if hasattr(auth_manager.provider, "get_region_endpoint"):
184
+ region_endpoint = auth_manager.provider.get_region_endpoint()
185
+ else:
186
+ settings = Settings()
187
+ region_endpoint = settings.region_endpoint
188
+
189
+ # Get sandbox mode from settings
190
+ settings = Settings()
191
+ sandbox_mode = settings.amazon_ads_sandbox_mode
192
+
193
+ response = {
194
+ "success": True,
195
+ "region": region,
196
+ "region_name": region_names.get(region, "Unknown"),
197
+ "api_endpoint": region_endpoint,
198
+ "sandbox_mode": sandbox_mode,
199
+ }
200
+
201
+ # Add OAuth endpoint if available
202
+ if hasattr(auth_manager.provider, "get_oauth_endpoint"):
203
+ response["oauth_endpoint"] = auth_manager.provider.get_oauth_endpoint()
204
+ response["auth_method"] = "direct"
205
+ else:
206
+ response["auth_method"] = "openbridge"
207
+
208
+ # Check if region is from identity (OpenBridge) or config
209
+ if auth_manager.get_active_identity():
210
+ identity_region = auth_manager.get_active_region()
211
+ if identity_region:
212
+ response["identity_region"] = identity_region
213
+ response["source"] = (
214
+ "identity" if identity_region == region else "config"
215
+ )
216
+ else:
217
+ response["source"] = "config"
218
+ else:
219
+ response["source"] = "config"
220
+
221
+ return response
222
+
223
+ except Exception as e:
224
+ logger.error(f"Failed to get active region: {e}")
225
+ raise
226
+
227
+
228
+ async def list_available_regions() -> dict:
229
+ """List available regions and their endpoints.
230
+
231
+ Include API and OAuth endpoints, marketplaces, sandbox mode, and the
232
+ currently selected region.
233
+
234
+ :return: Mapping containing region details.
235
+ :raises Exception: If listing fails.
236
+ """
237
+ try:
238
+ # Get auth manager to access current region
239
+ auth_manager = get_auth_manager()
240
+
241
+ # Get current region from provider or settings
242
+ if hasattr(auth_manager.provider, "region"):
243
+ current_region = auth_manager.provider.region
244
+ else:
245
+ settings = Settings()
246
+ current_region = settings.amazon_ads_region
247
+
248
+ # Get sandbox mode from settings
249
+ settings = Settings()
250
+ sandbox_mode = settings.amazon_ads_sandbox_mode
251
+
252
+ regions = {
253
+ "na": {
254
+ "name": "North America",
255
+ "api_endpoint": "https://advertising-api.amazon.com",
256
+ "oauth_endpoint": "https://api.amazon.com/auth/o2/token",
257
+ "marketplaces": ["US", "CA", "MX", "BR"],
258
+ },
259
+ "eu": {
260
+ "name": "Europe",
261
+ "api_endpoint": "https://advertising-api-eu.amazon.com",
262
+ "oauth_endpoint": "https://api.amazon.co.uk/auth/o2/token",
263
+ "marketplaces": [
264
+ "UK",
265
+ "DE",
266
+ "FR",
267
+ "IT",
268
+ "ES",
269
+ "NL",
270
+ "AE",
271
+ "SE",
272
+ "PL",
273
+ "TR",
274
+ "SG",
275
+ "AU",
276
+ "IN",
277
+ ],
278
+ },
279
+ "fe": {
280
+ "name": "Far East",
281
+ "api_endpoint": "https://advertising-api-fe.amazon.com",
282
+ "oauth_endpoint": "https://api.amazon.co.jp/auth/o2/token",
283
+ "marketplaces": ["JP"],
284
+ },
285
+ }
286
+
287
+ # Adjust for sandbox mode
288
+ if sandbox_mode:
289
+ for region_data in regions.values():
290
+ region_data["api_endpoint"] = region_data["api_endpoint"].replace(
291
+ "advertising-api", "advertising-api-test"
292
+ )
293
+ region_data["sandbox"] = True
294
+
295
+ return {
296
+ "success": True,
297
+ "current_region": current_region,
298
+ "sandbox_mode": sandbox_mode,
299
+ "regions": regions,
300
+ }
301
+
302
+ except Exception as e:
303
+ logger.error(f"Failed to list regions: {e}")
304
+ raise
305
+
306
+
307
+ # Alias functions for backward compatibility with builtin_tools
308
+ async def set_region(region: Literal["na", "eu", "fe"]) -> dict:
309
+ """Alias for set_active_region for backward compatibility."""
310
+ return await set_active_region(region)
311
+
312
+
313
+ async def get_region() -> dict:
314
+ """Alias for get_active_region for backward compatibility."""
315
+ return await get_active_region()
316
+
317
+
318
+ async def list_regions() -> dict:
319
+ """Alias for list_available_regions for backward compatibility."""
320
+ return await list_available_regions()
@@ -0,0 +1,175 @@
1
+ """Region-aware identity selection tools.
2
+
3
+ This module provides tools to help find and switch to identities
4
+ in specific regions when using OpenBridge authentication.
5
+ """
6
+
7
+ import logging
8
+ from typing import Dict, Optional
9
+
10
+ from ..auth.manager import get_auth_manager
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ async def list_identities_by_region(region: Optional[str] = None) -> Dict:
16
+ """List identities grouped by region.
17
+
18
+ This is particularly useful for OpenBridge authentication where
19
+ regions are tied to identities rather than being freely switchable.
20
+
21
+ :param region: Optional region filter ('na', 'eu', 'fe')
22
+ :type region: Optional[str]
23
+ :return: Dictionary with identities grouped by region
24
+ :rtype: Dict
25
+ """
26
+ try:
27
+ auth_manager = get_auth_manager()
28
+
29
+ # Get all identities
30
+ if hasattr(auth_manager.provider, "list_identities"):
31
+ all_identities = await auth_manager.provider.list_identities()
32
+ else:
33
+ all_identities = await auth_manager.list_identities()
34
+
35
+ # Group by region
36
+ by_region = {"na": [], "eu": [], "fe": [], "unknown": []}
37
+
38
+ for identity in all_identities:
39
+ identity_region = identity.attributes.get("region", "unknown").lower()
40
+ if identity_region in by_region:
41
+ by_region[identity_region].append(
42
+ {
43
+ "id": identity.id,
44
+ "name": identity.attributes.get("name", identity.id),
45
+ "email": identity.attributes.get("email", ""),
46
+ "region": identity_region,
47
+ "account_id": identity.attributes.get("account_id", ""),
48
+ }
49
+ )
50
+ else:
51
+ by_region["unknown"].append(
52
+ {
53
+ "id": identity.id,
54
+ "name": identity.attributes.get("name", identity.id),
55
+ "region": identity_region,
56
+ }
57
+ )
58
+
59
+ # Get current active identity
60
+ current_identity = auth_manager.get_active_identity()
61
+ current_id = current_identity.id if current_identity else None
62
+
63
+ # Filter by region if specified
64
+ if region and region in ["na", "eu", "fe"]:
65
+ filtered = {
66
+ region: by_region[region],
67
+ "total": len(by_region[region]),
68
+ "current_identity": current_id,
69
+ "message": f"Found {len(by_region[region])} identities in {region.upper()} region",
70
+ }
71
+ return filtered
72
+
73
+ # Return all grouped by region
74
+ return {
75
+ "regions": by_region,
76
+ "totals": {k: len(v) for k, v in by_region.items()},
77
+ "current_identity": current_id,
78
+ "message": "Identities grouped by region",
79
+ }
80
+
81
+ except Exception as e:
82
+ logger.error(f"Failed to list identities by region: {e}")
83
+ return {
84
+ "success": False,
85
+ "error": str(e),
86
+ "message": "Failed to list identities by region",
87
+ }
88
+
89
+
90
+ async def switch_to_region_identity(
91
+ target_region: str, identity_id: Optional[str] = None
92
+ ) -> Dict:
93
+ """Switch to an identity in the specified region.
94
+
95
+ If identity_id is provided, switches to that specific identity if it's in the
96
+ target region. Otherwise, switches to the first available identity in the region.
97
+
98
+ :param target_region: Target region ('na', 'eu', 'fe')
99
+ :type target_region: str
100
+ :param identity_id: Optional specific identity ID to switch to
101
+ :type identity_id: Optional[str]
102
+ :return: Result of the switch operation
103
+ :rtype: Dict
104
+ """
105
+ try:
106
+ if target_region not in ["na", "eu", "fe"]:
107
+ return {
108
+ "success": False,
109
+ "error": "INVALID_REGION",
110
+ "message": f"Invalid region: {target_region}. Must be 'na', 'eu', or 'fe'",
111
+ }
112
+
113
+ # Get identities in the target region
114
+ region_data = await list_identities_by_region(target_region)
115
+
116
+ if "error" in region_data:
117
+ return region_data
118
+
119
+ target_identities = region_data.get(target_region, [])
120
+
121
+ if not target_identities:
122
+ return {
123
+ "success": False,
124
+ "error": "NO_IDENTITIES",
125
+ "message": f"No identities found in {target_region.upper()} region",
126
+ "region": target_region,
127
+ }
128
+
129
+ # If specific identity requested, validate it's in the region
130
+ if identity_id:
131
+ matching = [i for i in target_identities if i["id"] == identity_id]
132
+ if not matching:
133
+ return {
134
+ "success": False,
135
+ "error": "IDENTITY_NOT_IN_REGION",
136
+ "message": f"Identity {identity_id} is not in {target_region.upper()} region",
137
+ "available_identities": target_identities,
138
+ }
139
+ selected = matching[0]
140
+ else:
141
+ # Select first available identity in region
142
+ selected = target_identities[0]
143
+
144
+ # Switch to the selected identity
145
+ from ..models import SetActiveIdentityRequest
146
+
147
+ request = SetActiveIdentityRequest(identity_id=selected["id"], persist=True)
148
+
149
+ from . import identity
150
+
151
+ result = await identity.set_active_identity(request)
152
+
153
+ if result.success:
154
+ return {
155
+ "success": True,
156
+ "message": f"Switched to {selected['name']} in {target_region.upper()} region",
157
+ "identity": selected,
158
+ "region": target_region,
159
+ "credentials_loaded": result.credentials_loaded,
160
+ }
161
+ else:
162
+ return {
163
+ "success": False,
164
+ "error": "SWITCH_FAILED",
165
+ "message": f"Failed to switch to identity: {result.message}",
166
+ "identity": selected,
167
+ }
168
+
169
+ except Exception as e:
170
+ logger.error(f"Failed to switch to region identity: {e}")
171
+ return {
172
+ "success": False,
173
+ "error": str(e),
174
+ "message": "Failed to switch to region identity",
175
+ }
@@ -0,0 +1,6 @@
1
+ """Utility functions for Amazon Ads API MCP server.
2
+
3
+ This module provides utility functions and classes for the Amazon Ads
4
+ MCP server, including OpenAPI specification handling and other
5
+ helper functionality.
6
+ """