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.
- amazon_ads_mcp/__init__.py +11 -0
- amazon_ads_mcp/auth/__init__.py +33 -0
- amazon_ads_mcp/auth/base.py +211 -0
- amazon_ads_mcp/auth/hooks.py +172 -0
- amazon_ads_mcp/auth/manager.py +791 -0
- amazon_ads_mcp/auth/oauth_state_store.py +277 -0
- amazon_ads_mcp/auth/providers/__init__.py +14 -0
- amazon_ads_mcp/auth/providers/direct.py +393 -0
- amazon_ads_mcp/auth/providers/example_auth0.py.example +216 -0
- amazon_ads_mcp/auth/providers/openbridge.py +512 -0
- amazon_ads_mcp/auth/registry.py +146 -0
- amazon_ads_mcp/auth/secure_token_store.py +297 -0
- amazon_ads_mcp/auth/token_store.py +723 -0
- amazon_ads_mcp/config/__init__.py +5 -0
- amazon_ads_mcp/config/sampling.py +111 -0
- amazon_ads_mcp/config/settings.py +366 -0
- amazon_ads_mcp/exceptions.py +314 -0
- amazon_ads_mcp/middleware/__init__.py +11 -0
- amazon_ads_mcp/middleware/authentication.py +1474 -0
- amazon_ads_mcp/middleware/caching.py +177 -0
- amazon_ads_mcp/middleware/oauth.py +175 -0
- amazon_ads_mcp/middleware/sampling.py +112 -0
- amazon_ads_mcp/models/__init__.py +320 -0
- amazon_ads_mcp/models/amc_models.py +837 -0
- amazon_ads_mcp/models/api_responses.py +847 -0
- amazon_ads_mcp/models/base_models.py +215 -0
- amazon_ads_mcp/models/builtin_responses.py +496 -0
- amazon_ads_mcp/models/dsp_models.py +556 -0
- amazon_ads_mcp/models/stores_brands.py +610 -0
- amazon_ads_mcp/server/__init__.py +6 -0
- amazon_ads_mcp/server/__main__.py +6 -0
- amazon_ads_mcp/server/builtin_prompts.py +269 -0
- amazon_ads_mcp/server/builtin_tools.py +962 -0
- amazon_ads_mcp/server/file_routes.py +547 -0
- amazon_ads_mcp/server/html_templates.py +149 -0
- amazon_ads_mcp/server/mcp_server.py +327 -0
- amazon_ads_mcp/server/openapi_utils.py +158 -0
- amazon_ads_mcp/server/sampling_handler.py +251 -0
- amazon_ads_mcp/server/server_builder.py +751 -0
- amazon_ads_mcp/server/sidecar_loader.py +178 -0
- amazon_ads_mcp/server/transform_executor.py +827 -0
- amazon_ads_mcp/tools/__init__.py +22 -0
- amazon_ads_mcp/tools/cache_management.py +105 -0
- amazon_ads_mcp/tools/download_tools.py +267 -0
- amazon_ads_mcp/tools/identity.py +236 -0
- amazon_ads_mcp/tools/oauth.py +598 -0
- amazon_ads_mcp/tools/profile.py +150 -0
- amazon_ads_mcp/tools/profile_listing.py +285 -0
- amazon_ads_mcp/tools/region.py +320 -0
- amazon_ads_mcp/tools/region_identity.py +175 -0
- amazon_ads_mcp/utils/__init__.py +6 -0
- amazon_ads_mcp/utils/async_compat.py +215 -0
- amazon_ads_mcp/utils/errors.py +452 -0
- amazon_ads_mcp/utils/export_content_type_resolver.py +249 -0
- amazon_ads_mcp/utils/export_download_handler.py +579 -0
- amazon_ads_mcp/utils/header_resolver.py +81 -0
- amazon_ads_mcp/utils/http/__init__.py +56 -0
- amazon_ads_mcp/utils/http/circuit_breaker.py +127 -0
- amazon_ads_mcp/utils/http/client_manager.py +329 -0
- amazon_ads_mcp/utils/http/request.py +207 -0
- amazon_ads_mcp/utils/http/resilience.py +512 -0
- amazon_ads_mcp/utils/http/resilient_client.py +195 -0
- amazon_ads_mcp/utils/http/retry.py +76 -0
- amazon_ads_mcp/utils/http_client.py +873 -0
- amazon_ads_mcp/utils/media/__init__.py +21 -0
- amazon_ads_mcp/utils/media/negotiator.py +243 -0
- amazon_ads_mcp/utils/media/types.py +199 -0
- amazon_ads_mcp/utils/openapi/__init__.py +16 -0
- amazon_ads_mcp/utils/openapi/json.py +55 -0
- amazon_ads_mcp/utils/openapi/loader.py +263 -0
- amazon_ads_mcp/utils/openapi/refs.py +46 -0
- amazon_ads_mcp/utils/region_config.py +200 -0
- amazon_ads_mcp/utils/response_wrapper.py +171 -0
- amazon_ads_mcp/utils/sampling_helpers.py +156 -0
- amazon_ads_mcp/utils/sampling_wrapper.py +173 -0
- amazon_ads_mcp/utils/security.py +630 -0
- amazon_ads_mcp/utils/tool_naming.py +137 -0
- amazon_ads_mcp-0.2.7.dist-info/METADATA +664 -0
- amazon_ads_mcp-0.2.7.dist-info/RECORD +82 -0
- amazon_ads_mcp-0.2.7.dist-info/WHEEL +4 -0
- amazon_ads_mcp-0.2.7.dist-info/entry_points.txt +3 -0
- amazon_ads_mcp-0.2.7.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Download/Export content-type resolver for Amazon Ads API.
|
|
3
|
+
|
|
4
|
+
This module provides helpers to determine preferred Accept/Content-Type
|
|
5
|
+
for known download-like flows across Amazon Ads APIs, beyond classic
|
|
6
|
+
Exports (e.g., DSP Measurement results, Sponsored Ads report/snapshot
|
|
7
|
+
redirectors, Brand Metrics report retrieval).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import base64
|
|
11
|
+
import logging
|
|
12
|
+
from typing import List, Optional
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
def _decode_export_id(export_id: str) -> Optional[str]:
|
|
17
|
+
"""Decode an export ID payload to text, if it looks base64-encoded."""
|
|
18
|
+
if not export_id:
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
pad_len = (-len(export_id)) % 4
|
|
22
|
+
padded = export_id + ("=" * pad_len)
|
|
23
|
+
|
|
24
|
+
for decoder in (base64.urlsafe_b64decode, base64.b64decode):
|
|
25
|
+
try:
|
|
26
|
+
decoded_bytes = decoder(padded)
|
|
27
|
+
return decoded_bytes.decode("utf-8")
|
|
28
|
+
except Exception:
|
|
29
|
+
continue
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def resolve_export_content_type(export_id: str) -> Optional[str]:
|
|
34
|
+
"""
|
|
35
|
+
Resolve the correct content-type for an export based on its ID.
|
|
36
|
+
|
|
37
|
+
Amazon export IDs appear to be base64-encoded with a suffix indicating type:
|
|
38
|
+
- ,C = Campaign export
|
|
39
|
+
- ,A = Ad Group export
|
|
40
|
+
- ,AD = Ads export
|
|
41
|
+
- ,T = Targets export
|
|
42
|
+
|
|
43
|
+
:param export_id: The export ID from Amazon
|
|
44
|
+
:type export_id: str
|
|
45
|
+
:return: The appropriate content-type or None if unable to determine
|
|
46
|
+
:rtype: Optional[str]
|
|
47
|
+
"""
|
|
48
|
+
if not export_id:
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
# Export IDs appear to be base64 encoded with a type suffix
|
|
53
|
+
# Example: "OTc2MDhjNmEtNDg3Zi00YzMyLTllOWEtMDMwNjNhYTk1MGM0LEM"
|
|
54
|
+
# Decodes to: "97608c6a-487f-4c32-9e9a-03063aa950c4,C"
|
|
55
|
+
|
|
56
|
+
# Try to decode to check for pattern
|
|
57
|
+
try:
|
|
58
|
+
decoded = _decode_export_id(export_id)
|
|
59
|
+
if not decoded:
|
|
60
|
+
raise ValueError("Export ID did not decode")
|
|
61
|
+
if "," in decoded:
|
|
62
|
+
_, suffix = decoded.rsplit(",", 1)
|
|
63
|
+
suffix = suffix.upper()
|
|
64
|
+
|
|
65
|
+
# Map suffix to content-type
|
|
66
|
+
suffix_map = {
|
|
67
|
+
"C": "application/vnd.campaignsexport.v1+json",
|
|
68
|
+
"A": "application/vnd.adgroupsexport.v1+json",
|
|
69
|
+
"AD": "application/vnd.adsexport.v1+json",
|
|
70
|
+
# Some export IDs use ',R' for ads exports.
|
|
71
|
+
"R": "application/vnd.adsexport.v1+json",
|
|
72
|
+
"T": "application/vnd.targetsexport.v1+json",
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
content_type = suffix_map.get(suffix)
|
|
76
|
+
if content_type:
|
|
77
|
+
logger.debug(
|
|
78
|
+
f"Resolved export type from ID suffix '{suffix}': {content_type}"
|
|
79
|
+
)
|
|
80
|
+
return content_type
|
|
81
|
+
except Exception:
|
|
82
|
+
# Not base64 or doesn't match expected pattern
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
# Fallback: check if the ID itself contains hints
|
|
86
|
+
export_id_lower = export_id.lower()
|
|
87
|
+
|
|
88
|
+
if "campaign" in export_id_lower:
|
|
89
|
+
return "application/vnd.campaignsexport.v1+json"
|
|
90
|
+
elif "adgroup" in export_id_lower:
|
|
91
|
+
return "application/vnd.adgroupsexport.v1+json"
|
|
92
|
+
elif "ad" in export_id_lower and "adgroup" not in export_id_lower:
|
|
93
|
+
return "application/vnd.adsexport.v1+json"
|
|
94
|
+
elif "target" in export_id_lower:
|
|
95
|
+
return "application/vnd.targetsexport.v1+json"
|
|
96
|
+
|
|
97
|
+
except Exception as e:
|
|
98
|
+
logger.warning(f"Error resolving export content-type: {e}")
|
|
99
|
+
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def get_export_accept_headers(export_id: str) -> List[str]:
|
|
104
|
+
"""
|
|
105
|
+
Get a prioritized list of Accept headers for an export.
|
|
106
|
+
|
|
107
|
+
If we can determine the type, return that first.
|
|
108
|
+
Otherwise return all possibilities in a sensible order.
|
|
109
|
+
|
|
110
|
+
:param export_id: The export ID
|
|
111
|
+
:type export_id: str
|
|
112
|
+
:return: List of content-types to try
|
|
113
|
+
:rtype: List[str]
|
|
114
|
+
"""
|
|
115
|
+
# Try to resolve the specific type
|
|
116
|
+
specific_type = resolve_export_content_type(export_id)
|
|
117
|
+
|
|
118
|
+
# All possible types
|
|
119
|
+
all_types = [
|
|
120
|
+
"application/vnd.campaignsexport.v1+json",
|
|
121
|
+
"application/vnd.adgroupsexport.v1+json",
|
|
122
|
+
"application/vnd.adsexport.v1+json",
|
|
123
|
+
"application/vnd.targetsexport.v1+json",
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
if specific_type and specific_type in all_types:
|
|
127
|
+
# Put the specific type first, then other vendor types, then application/json
|
|
128
|
+
# FastMCP's experimental parser needs application/json in the Accept list
|
|
129
|
+
all_types.remove(specific_type)
|
|
130
|
+
return [specific_type] + all_types + ["application/json"]
|
|
131
|
+
|
|
132
|
+
# Include application/json as fallback for FastMCP compatibility
|
|
133
|
+
return all_types + ["application/json"]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def get_measurement_accept_headers(prefer_csv: bool = False) -> List[str]:
|
|
137
|
+
"""
|
|
138
|
+
Preferred Accept headers for DSP Measurement result downloads.
|
|
139
|
+
|
|
140
|
+
Measurement endpoints support both JSON and CSV vendor types, with a 307
|
|
141
|
+
redirect to an S3 location for actual file download. If CSV is preferred,
|
|
142
|
+
return CSV first; otherwise JSON first.
|
|
143
|
+
"""
|
|
144
|
+
json_type = "application/vnd.measurementresult.v1.2+json"
|
|
145
|
+
csv_type = "text/vnd.measurementresult.v1.2+csv"
|
|
146
|
+
# Include application/json for FastMCP compatibility
|
|
147
|
+
types = [csv_type, json_type] if prefer_csv else [json_type, csv_type]
|
|
148
|
+
return types + ["application/json"]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def get_brandmetrics_accept_headers() -> List[str]:
|
|
152
|
+
"""
|
|
153
|
+
Preferred Accept headers for Brand Metrics report endpoints.
|
|
154
|
+
|
|
155
|
+
Brand Metrics typically returns vendor JSON, with multiple versions
|
|
156
|
+
(v1.1 and v1). Prefer the latest first.
|
|
157
|
+
"""
|
|
158
|
+
return [
|
|
159
|
+
"application/vnd.insightsbrandmetrics.v1.1+json",
|
|
160
|
+
"application/vnd.insightsbrandmetrics.v1+json",
|
|
161
|
+
"application/json", # Include for FastMCP compatibility
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def get_reports_download_accept_headers() -> List[str]:
|
|
166
|
+
"""
|
|
167
|
+
Sponsored Ads reports/snapshots download endpoints return a 307 redirect
|
|
168
|
+
with location header. Accept is usually application/json for the status
|
|
169
|
+
envelope prior to redirect.
|
|
170
|
+
"""
|
|
171
|
+
return ["application/json"]
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def resolve_download_accept_headers(
|
|
175
|
+
method: str, url: str, *, prefer_csv: bool = False
|
|
176
|
+
) -> List[str]:
|
|
177
|
+
"""
|
|
178
|
+
Resolve Accept headers for known download endpoints using URL heuristics.
|
|
179
|
+
|
|
180
|
+
- Exports: use export-specific vendor types inferred from export_id where possible.
|
|
181
|
+
- DSP Measurement: support JSON/CSV vendor types (prefer CSV if requested).
|
|
182
|
+
- Sponsored Ads Reports/Snapshots: default to application/json (redirect flow).
|
|
183
|
+
- Brand Metrics: prefer v1.1 then v1 vendor JSON.
|
|
184
|
+
- S3 URLs: No Accept override (let S3 handle it)
|
|
185
|
+
"""
|
|
186
|
+
m = (method or "GET").upper()
|
|
187
|
+
u = (url or "").lower()
|
|
188
|
+
|
|
189
|
+
# Don't override Accept for S3 URLs - they serve pre-defined content
|
|
190
|
+
from urllib.parse import urlparse
|
|
191
|
+
|
|
192
|
+
parsed = urlparse(url)
|
|
193
|
+
# Validate hostname is legitimate AWS domain
|
|
194
|
+
hostname = (parsed.hostname or "").lower()
|
|
195
|
+
is_aws_domain = hostname.endswith(".amazonaws.com") or hostname == "amazonaws.com"
|
|
196
|
+
if hostname and is_aws_domain:
|
|
197
|
+
# S3 URLs don't need Accept headers - they serve what they have
|
|
198
|
+
return []
|
|
199
|
+
|
|
200
|
+
# Export creation endpoints (POST)
|
|
201
|
+
if m == "POST" and "/export" in u:
|
|
202
|
+
if "/campaigns/export" in u:
|
|
203
|
+
return [
|
|
204
|
+
"application/vnd.campaignsexport.v1+json",
|
|
205
|
+
"application/json",
|
|
206
|
+
]
|
|
207
|
+
elif "/adgroups/export" in u:
|
|
208
|
+
return [
|
|
209
|
+
"application/vnd.adgroupsexport.v1+json",
|
|
210
|
+
"application/json",
|
|
211
|
+
]
|
|
212
|
+
elif "/ads/export" in u:
|
|
213
|
+
return ["application/vnd.adsexport.v1+json", "application/json"]
|
|
214
|
+
elif "/targets/export" in u:
|
|
215
|
+
return [
|
|
216
|
+
"application/vnd.targetsexport.v1+json",
|
|
217
|
+
"application/json",
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
# Export retrieval pattern: /exports/{exportId}
|
|
221
|
+
if "/exports/" in u:
|
|
222
|
+
# Try to extract exportId and leverage export content-type map
|
|
223
|
+
try:
|
|
224
|
+
import re
|
|
225
|
+
|
|
226
|
+
match = re.search(r"/exports/([^/?]+)", url)
|
|
227
|
+
if match:
|
|
228
|
+
export_id = match.group(1)
|
|
229
|
+
return get_export_accept_headers(export_id)
|
|
230
|
+
except Exception:
|
|
231
|
+
pass
|
|
232
|
+
return get_export_accept_headers("")
|
|
233
|
+
|
|
234
|
+
# DSP Measurement results (audienceResearch/brandLift/creativeTesting/etc.)
|
|
235
|
+
if "/dsp/measurement/" in u or "/measurement/" in u:
|
|
236
|
+
return get_measurement_accept_headers(prefer_csv=prefer_csv)
|
|
237
|
+
|
|
238
|
+
# Sponsored Display/Snapshots/Reports download endpoints
|
|
239
|
+
if "/snapshots/" in u and "/download" in u:
|
|
240
|
+
return get_reports_download_accept_headers()
|
|
241
|
+
if "/v2/reports/" in u and "/download" in u:
|
|
242
|
+
return get_reports_download_accept_headers()
|
|
243
|
+
|
|
244
|
+
# Brand Metrics
|
|
245
|
+
if "/insights/brandmetrics/" in u or "brandmetrics" in u:
|
|
246
|
+
return get_brandmetrics_accept_headers()
|
|
247
|
+
|
|
248
|
+
# Default fallback
|
|
249
|
+
return ["application/json"]
|