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,111 @@
|
|
|
1
|
+
"""Sampling configuration for server-side LLM fallback."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SamplingConfig(BaseModel):
|
|
13
|
+
"""Configuration for optional server-side sampling."""
|
|
14
|
+
|
|
15
|
+
enabled: bool = Field(
|
|
16
|
+
default=False, description="Enable server-side sampling fallback"
|
|
17
|
+
)
|
|
18
|
+
provider: str = Field(default="openai", description="LLM provider (openai or none)")
|
|
19
|
+
model: str = Field(default="gpt-4o-mini", description="Model to use for sampling")
|
|
20
|
+
api_key: Optional[str] = Field(default=None, description="API key for the provider")
|
|
21
|
+
base_url: Optional[str] = Field(
|
|
22
|
+
default=None,
|
|
23
|
+
description="Optional base URL for OpenAI-compatible endpoints",
|
|
24
|
+
)
|
|
25
|
+
temperature: float = Field(default=0.2, description="Sampling temperature")
|
|
26
|
+
max_tokens: int = Field(default=400, description="Maximum tokens for sampling")
|
|
27
|
+
timeout_ms: int = Field(default=8000, description="Timeout in milliseconds")
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def from_environment(cls) -> "SamplingConfig":
|
|
31
|
+
"""Load configuration from environment variables."""
|
|
32
|
+
config = cls()
|
|
33
|
+
|
|
34
|
+
# Check if sampling is enabled
|
|
35
|
+
sampling_enabled = os.getenv("SAMPLING_ENABLED")
|
|
36
|
+
if sampling_enabled and sampling_enabled.lower() in (
|
|
37
|
+
"true",
|
|
38
|
+
"1",
|
|
39
|
+
"yes",
|
|
40
|
+
):
|
|
41
|
+
config.enabled = True
|
|
42
|
+
logger.info("Server-side sampling is ENABLED")
|
|
43
|
+
|
|
44
|
+
# Load provider settings
|
|
45
|
+
if provider := os.getenv("SAMPLING_PROVIDER"):
|
|
46
|
+
config.provider = provider.lower()
|
|
47
|
+
|
|
48
|
+
if model := os.getenv("SAMPLING_MODEL"):
|
|
49
|
+
config.model = model
|
|
50
|
+
|
|
51
|
+
# Use standard OpenAI API key
|
|
52
|
+
config.api_key = os.getenv("OPENAI_API_KEY")
|
|
53
|
+
|
|
54
|
+
if base_url := os.getenv("SAMPLING_BASE_URL"):
|
|
55
|
+
config.base_url = base_url
|
|
56
|
+
|
|
57
|
+
# Load sampling parameters
|
|
58
|
+
if temp := os.getenv("SAMPLING_TEMPERATURE"):
|
|
59
|
+
try:
|
|
60
|
+
config.temperature = float(temp)
|
|
61
|
+
except ValueError:
|
|
62
|
+
logger.warning(
|
|
63
|
+
f"Invalid temperature value: {temp}, using default {config.temperature}"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if max_tok := os.getenv("SAMPLING_MAX_TOKENS"):
|
|
67
|
+
try:
|
|
68
|
+
config.max_tokens = int(max_tok)
|
|
69
|
+
except ValueError:
|
|
70
|
+
logger.warning(
|
|
71
|
+
f"Invalid max_tokens value: {max_tok}, using default {config.max_tokens}"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if timeout := os.getenv("SAMPLING_TIMEOUT_MS"):
|
|
75
|
+
try:
|
|
76
|
+
config.timeout_ms = int(timeout)
|
|
77
|
+
except ValueError:
|
|
78
|
+
logger.warning(
|
|
79
|
+
f"Invalid timeout_ms value: {timeout}, using default {config.timeout_ms}"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return config
|
|
83
|
+
|
|
84
|
+
def is_valid(self) -> bool:
|
|
85
|
+
"""Check if the configuration is valid for creating a sampling handler."""
|
|
86
|
+
if not self.enabled:
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
if self.provider == "none":
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
if self.provider == "openai" and not self.api_key:
|
|
93
|
+
logger.warning(
|
|
94
|
+
"Sampling enabled but no API key provided. Sampling will be disabled."
|
|
95
|
+
)
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
return True
|
|
99
|
+
|
|
100
|
+
def log_status(self):
|
|
101
|
+
"""Log the current sampling configuration status."""
|
|
102
|
+
if not self.enabled:
|
|
103
|
+
logger.info("Server-side sampling: DISABLED (default)")
|
|
104
|
+
elif not self.is_valid():
|
|
105
|
+
logger.warning("Server-side sampling: DISABLED (invalid configuration)")
|
|
106
|
+
else:
|
|
107
|
+
logger.info(
|
|
108
|
+
"Server-side sampling: ENABLED (provider=%s, model=%s, fallback-only)",
|
|
109
|
+
self.provider,
|
|
110
|
+
self.model,
|
|
111
|
+
)
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
"""Configuration settings for Amazon Ads API MCP server.
|
|
2
|
+
|
|
3
|
+
This module defines the configuration settings for the Amazon Ads MCP server,
|
|
4
|
+
including authentication methods, API endpoints, and server configuration.
|
|
5
|
+
Settings are loaded from environment variables and .env files.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Literal, Optional
|
|
9
|
+
|
|
10
|
+
from pydantic import AliasChoices, Field, field_validator
|
|
11
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
12
|
+
|
|
13
|
+
from ..utils.region_config import RegionConfig
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Settings(BaseSettings):
|
|
17
|
+
"""Application settings loaded from environment variables.
|
|
18
|
+
|
|
19
|
+
Manages all configuration settings for the Amazon Ads MCP server.
|
|
20
|
+
Supports both direct Amazon Ads API authentication and OpenBridge
|
|
21
|
+
integration. Settings can be configured via environment variables
|
|
22
|
+
or .env files.
|
|
23
|
+
|
|
24
|
+
:param auth_method: Authentication method to use (direct/openbridge)
|
|
25
|
+
:type auth_method: Literal["direct", "openbridge"]
|
|
26
|
+
:param amazon_ads_client_id: Amazon Ads API Client ID for direct auth
|
|
27
|
+
:type amazon_ads_client_id: Optional[str]
|
|
28
|
+
:param amazon_ads_client_secret: Amazon Ads API Client Secret for direct auth
|
|
29
|
+
:type amazon_ads_client_secret: Optional[str]
|
|
30
|
+
:param amazon_ads_refresh_token: Amazon Ads API Refresh Token for direct auth
|
|
31
|
+
:type amazon_ads_refresh_token: Optional[str]
|
|
32
|
+
:param openbridge_refresh_token: OpenBridge API key (aka refresh token)
|
|
33
|
+
:type openbridge_refresh_token: Optional[str]
|
|
34
|
+
:param openbridge_remote_identity_id: OpenBridge remote identity ID
|
|
35
|
+
:type openbridge_remote_identity_id: Optional[str]
|
|
36
|
+
:param amazon_ads_region: Amazon Ads API region (na/eu/fe)
|
|
37
|
+
:type amazon_ads_region: Literal["na", "eu", "fe"]
|
|
38
|
+
:param amazon_ads_api_base_url: Base URL for Amazon Ads API
|
|
39
|
+
:type amazon_ads_api_base_url: str
|
|
40
|
+
:param amazon_ads_sandbox_mode: Enable sandbox mode for testing
|
|
41
|
+
:type amazon_ads_sandbox_mode: bool
|
|
42
|
+
:param mcp_server_name: Name of the MCP server
|
|
43
|
+
:type mcp_server_name: str
|
|
44
|
+
:param mcp_server_host: Host for the MCP server
|
|
45
|
+
:type mcp_server_host: str
|
|
46
|
+
:param mcp_server_port: Port for the MCP server
|
|
47
|
+
:type mcp_server_port: int
|
|
48
|
+
:param log_level: Logging level for the application
|
|
49
|
+
:type log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
model_config = SettingsConfigDict(
|
|
53
|
+
env_file=".env",
|
|
54
|
+
env_file_encoding="utf-8",
|
|
55
|
+
case_sensitive=False,
|
|
56
|
+
extra="ignore", # Ignore extra fields in .env file
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Authentication method
|
|
60
|
+
auth_method: Literal["direct", "openbridge"] = Field(
|
|
61
|
+
"openbridge", description="Authentication method to use"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Direct Amazon Ads API Configuration (BYOA - Bring Your Own API)
|
|
65
|
+
# These match the standard Amazon Ads API environment variables
|
|
66
|
+
ad_api_client_id: Optional[str] = Field(
|
|
67
|
+
None,
|
|
68
|
+
alias="AMAZON_AD_API_CLIENT_ID",
|
|
69
|
+
description="Amazon Ads API Client ID (for direct auth)",
|
|
70
|
+
)
|
|
71
|
+
ad_api_client_secret: Optional[str] = Field(
|
|
72
|
+
None,
|
|
73
|
+
alias="AMAZON_AD_API_CLIENT_SECRET",
|
|
74
|
+
description="Amazon Ads API Client Secret (for direct auth)",
|
|
75
|
+
)
|
|
76
|
+
ad_api_refresh_token: Optional[str] = Field(
|
|
77
|
+
None,
|
|
78
|
+
alias="AMAZON_AD_API_REFRESH_TOKEN",
|
|
79
|
+
description="Amazon Ads API Refresh Token (for direct auth)",
|
|
80
|
+
)
|
|
81
|
+
ad_api_profile_id: Optional[str] = Field(
|
|
82
|
+
None,
|
|
83
|
+
alias="AMAZON_AD_API_PROFILE_ID",
|
|
84
|
+
description="Amazon Ads Profile ID (for direct auth)",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Legacy field names for backward compatibility
|
|
88
|
+
amazon_ads_client_id: Optional[str] = Field(
|
|
89
|
+
None,
|
|
90
|
+
description="Amazon Ads API Client ID (deprecated, use AMAZON_AD_API_CLIENT_ID)",
|
|
91
|
+
)
|
|
92
|
+
amazon_ads_client_secret: Optional[str] = Field(
|
|
93
|
+
None,
|
|
94
|
+
description="Amazon Ads API Client Secret (deprecated, use AMAZON_AD_API_CLIENT_SECRET)",
|
|
95
|
+
)
|
|
96
|
+
amazon_ads_refresh_token: Optional[str] = Field(
|
|
97
|
+
None,
|
|
98
|
+
description="Amazon Ads API Refresh Token (deprecated, use AMAZON_AD_API_REFRESH_TOKEN)",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Openbridge Configuration
|
|
102
|
+
openbridge_refresh_token: Optional[str] = Field(
|
|
103
|
+
None,
|
|
104
|
+
validation_alias=AliasChoices("OPENBRIDGE_REFRESH_TOKEN", "OPENBRIDGE_API_KEY"),
|
|
105
|
+
description="OpenBridge API key (aka refresh token)",
|
|
106
|
+
)
|
|
107
|
+
openbridge_remote_identity_id: Optional[str] = Field(
|
|
108
|
+
None, description="Openbridge Remote Identity ID for Amazon Ads"
|
|
109
|
+
)
|
|
110
|
+
amazon_ads_region: Literal["na", "eu", "fe"] = Field(
|
|
111
|
+
"na", description="Amazon Ads API Region"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# API Configuration
|
|
115
|
+
amazon_ads_api_base_url: str = Field(
|
|
116
|
+
"https://advertising-api.amazon.com",
|
|
117
|
+
description="Amazon Ads API Base URL",
|
|
118
|
+
)
|
|
119
|
+
amazon_ads_sandbox_mode: bool = Field(
|
|
120
|
+
False, description="Enable sandbox mode for testing"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# MCP Server Configuration
|
|
124
|
+
mcp_server_name: str = Field("amazon-ads-api", description="MCP Server Name")
|
|
125
|
+
mcp_server_host: str = Field("localhost", description="MCP Server Host")
|
|
126
|
+
mcp_server_port: int = Field(8000, description="MCP Server Port")
|
|
127
|
+
|
|
128
|
+
# Runtime configuration (set by CLI or Docker)
|
|
129
|
+
port: Optional[int] = Field(
|
|
130
|
+
None, description="HTTP server port (from PORT env var or CLI)"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Logging
|
|
134
|
+
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = Field(
|
|
135
|
+
"INFO", description="Logging level"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# FastMCP Configuration
|
|
139
|
+
fastmcp_use_experimental_parser: bool = Field(
|
|
140
|
+
True,
|
|
141
|
+
description="Enable FastMCP experimental OpenAPI parser for better performance",
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# OAuth Configuration (optional, for web-based authentication flows)
|
|
145
|
+
oauth_client_id: Optional[str] = Field(
|
|
146
|
+
None,
|
|
147
|
+
alias="OAUTH_CLIENT_ID",
|
|
148
|
+
description="OAuth Client ID for web authentication flow",
|
|
149
|
+
)
|
|
150
|
+
oauth_client_secret: Optional[str] = Field(
|
|
151
|
+
None,
|
|
152
|
+
alias="OAUTH_CLIENT_SECRET",
|
|
153
|
+
description="OAuth Client Secret for web authentication flow",
|
|
154
|
+
)
|
|
155
|
+
oauth_redirect_uri: Optional[str] = Field(
|
|
156
|
+
None,
|
|
157
|
+
alias="OAUTH_REDIRECT_URI",
|
|
158
|
+
description="OAuth Redirect URI for web authentication flow",
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Sampling Configuration (optional, for testing)
|
|
162
|
+
enable_sampling: bool = Field(
|
|
163
|
+
False,
|
|
164
|
+
alias="SAMPLING_ENABLED",
|
|
165
|
+
description="Enable request/response sampling for testing",
|
|
166
|
+
)
|
|
167
|
+
sampling_config_path: Optional[str] = Field(
|
|
168
|
+
None,
|
|
169
|
+
alias="SAMPLING_CONFIG_PATH",
|
|
170
|
+
description="Path to sampling configuration file",
|
|
171
|
+
)
|
|
172
|
+
default_sampling_rate: float = Field(
|
|
173
|
+
0.1,
|
|
174
|
+
alias="DEFAULT_SAMPLING_RATE",
|
|
175
|
+
description="Default sampling rate (0.0-1.0)",
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Response Caching Configuration
|
|
179
|
+
enable_response_caching: bool = Field(
|
|
180
|
+
False,
|
|
181
|
+
alias="ENABLE_RESPONSE_CACHING",
|
|
182
|
+
description="Enable response caching for safe read-only tools",
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# File Download Configuration
|
|
186
|
+
download_auth_token: Optional[str] = Field(
|
|
187
|
+
None,
|
|
188
|
+
validation_alias=AliasChoices(
|
|
189
|
+
"AMAZON_ADS_DOWNLOAD_AUTH_TOKEN", "DOWNLOAD_AUTH_TOKEN"
|
|
190
|
+
),
|
|
191
|
+
description="Bearer token for authenticating download requests (optional)",
|
|
192
|
+
)
|
|
193
|
+
download_max_file_size: int = Field(
|
|
194
|
+
512 * 1024 * 1024, # 512MB default
|
|
195
|
+
alias="AMAZON_ADS_DOWNLOAD_MAX_FILE_SIZE",
|
|
196
|
+
description="Maximum file size in bytes for downloads (default 512MB)",
|
|
197
|
+
)
|
|
198
|
+
download_allowed_extensions: Optional[str] = Field(
|
|
199
|
+
None,
|
|
200
|
+
alias="AMAZON_ADS_DOWNLOAD_ALLOWED_EXTENSIONS",
|
|
201
|
+
description="Comma-separated list of allowed file extensions (e.g., '.json,.csv,.gz')",
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
@field_validator("auth_method")
|
|
205
|
+
@classmethod
|
|
206
|
+
def auto_detect_auth_method(cls, v: str, info) -> str:
|
|
207
|
+
"""Auto-detect authentication method based on available credentials.
|
|
208
|
+
|
|
209
|
+
If direct Amazon Ads API credentials are provided, automatically
|
|
210
|
+
switch to 'direct' auth method. Prioritizes direct credentials
|
|
211
|
+
over OpenBridge when both are available.
|
|
212
|
+
|
|
213
|
+
:param v: The original auth method value
|
|
214
|
+
:type v: str
|
|
215
|
+
:param info: Validation info containing other field values
|
|
216
|
+
:type info: Any
|
|
217
|
+
:return: Detected or original authentication method
|
|
218
|
+
:rtype: str
|
|
219
|
+
"""
|
|
220
|
+
# Get the credential values
|
|
221
|
+
client_id = info.data.get("ad_api_client_id") or info.data.get(
|
|
222
|
+
"amazon_ads_client_id"
|
|
223
|
+
)
|
|
224
|
+
client_secret = info.data.get("ad_api_client_secret") or info.data.get(
|
|
225
|
+
"amazon_ads_client_secret"
|
|
226
|
+
)
|
|
227
|
+
refresh_token = info.data.get("ad_api_refresh_token") or info.data.get(
|
|
228
|
+
"amazon_ads_refresh_token"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# Check for direct API credentials (all three must be present and non-empty)
|
|
232
|
+
has_direct_creds = all(
|
|
233
|
+
[
|
|
234
|
+
client_id and client_id.strip(),
|
|
235
|
+
client_secret and client_secret.strip(),
|
|
236
|
+
refresh_token and refresh_token.strip(),
|
|
237
|
+
]
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
if has_direct_creds:
|
|
241
|
+
return "direct"
|
|
242
|
+
|
|
243
|
+
# Check for OpenBridge credentials
|
|
244
|
+
has_openbridge = info.data.get("openbridge_refresh_token")
|
|
245
|
+
if has_openbridge and has_openbridge.strip():
|
|
246
|
+
return "openbridge"
|
|
247
|
+
|
|
248
|
+
# Check for partial direct credentials to provide helpful error
|
|
249
|
+
has_partial_direct = any([client_id, client_secret, refresh_token])
|
|
250
|
+
if has_partial_direct and not has_openbridge:
|
|
251
|
+
missing = []
|
|
252
|
+
if not client_id or not client_id.strip():
|
|
253
|
+
missing.append("AMAZON_AD_API_CLIENT_ID")
|
|
254
|
+
if not client_secret or not client_secret.strip():
|
|
255
|
+
missing.append("AMAZON_AD_API_CLIENT_SECRET")
|
|
256
|
+
if not refresh_token or not refresh_token.strip():
|
|
257
|
+
missing.append("AMAZON_AD_API_REFRESH_TOKEN")
|
|
258
|
+
|
|
259
|
+
if missing:
|
|
260
|
+
# Return the original value, the provider setup will give detailed error
|
|
261
|
+
return v
|
|
262
|
+
|
|
263
|
+
return v # Return the original value if no credentials detected
|
|
264
|
+
|
|
265
|
+
@field_validator("amazon_ads_api_base_url")
|
|
266
|
+
@classmethod
|
|
267
|
+
def validate_api_base_url(cls, v: str, info) -> str:
|
|
268
|
+
"""Adjust API URL based on sandbox mode.
|
|
269
|
+
|
|
270
|
+
Automatically modifies the API base URL to use the test endpoint
|
|
271
|
+
when sandbox mode is enabled.
|
|
272
|
+
|
|
273
|
+
:param v: The original API base URL
|
|
274
|
+
:type v: str
|
|
275
|
+
:param info: Validation info containing other field values
|
|
276
|
+
:type info: Any
|
|
277
|
+
:return: Modified API base URL for sandbox mode if enabled
|
|
278
|
+
:rtype: str
|
|
279
|
+
"""
|
|
280
|
+
if info.data.get("amazon_ads_sandbox_mode"):
|
|
281
|
+
return v.replace("advertising-api", "advertising-api-test")
|
|
282
|
+
return v
|
|
283
|
+
|
|
284
|
+
@property
|
|
285
|
+
def effective_client_id(self) -> Optional[str]:
|
|
286
|
+
"""Get the effective client ID (new or legacy).
|
|
287
|
+
|
|
288
|
+
Returns the client ID from either the new AMAZON_AD_API_CLIENT_ID
|
|
289
|
+
environment variable or the legacy amazon_ads_client_id.
|
|
290
|
+
|
|
291
|
+
:return: Effective client ID or None
|
|
292
|
+
:rtype: Optional[str]
|
|
293
|
+
"""
|
|
294
|
+
return self.ad_api_client_id or self.amazon_ads_client_id
|
|
295
|
+
|
|
296
|
+
@property
|
|
297
|
+
def effective_client_secret(self) -> Optional[str]:
|
|
298
|
+
"""Get the effective client secret (new or legacy).
|
|
299
|
+
|
|
300
|
+
Returns the client secret from either the new AMAZON_AD_API_CLIENT_SECRET
|
|
301
|
+
environment variable or the legacy amazon_ads_client_secret.
|
|
302
|
+
|
|
303
|
+
:return: Effective client secret or None
|
|
304
|
+
:rtype: Optional[str]
|
|
305
|
+
"""
|
|
306
|
+
return self.ad_api_client_secret or self.amazon_ads_client_secret
|
|
307
|
+
|
|
308
|
+
@property
|
|
309
|
+
def effective_refresh_token(self) -> Optional[str]:
|
|
310
|
+
"""Get the effective refresh token (new or legacy).
|
|
311
|
+
|
|
312
|
+
Returns the refresh token from either the new AMAZON_AD_API_REFRESH_TOKEN
|
|
313
|
+
environment variable or the legacy amazon_ads_refresh_token.
|
|
314
|
+
|
|
315
|
+
NOTE: Using refresh tokens via environment variables is deprecated.
|
|
316
|
+
Prefer OAuth flow or secure token storage for production use.
|
|
317
|
+
|
|
318
|
+
:return: Effective refresh token or None
|
|
319
|
+
:rtype: Optional[str]
|
|
320
|
+
"""
|
|
321
|
+
token = self.ad_api_refresh_token or self.amazon_ads_refresh_token
|
|
322
|
+
if token:
|
|
323
|
+
import warnings
|
|
324
|
+
|
|
325
|
+
warnings.warn(
|
|
326
|
+
"Using refresh tokens via environment variables is deprecated and insecure. "
|
|
327
|
+
"Please use OAuth flow or secure token storage instead.",
|
|
328
|
+
DeprecationWarning,
|
|
329
|
+
stacklevel=2,
|
|
330
|
+
)
|
|
331
|
+
return token
|
|
332
|
+
|
|
333
|
+
@property
|
|
334
|
+
def effective_profile_id(self) -> Optional[str]:
|
|
335
|
+
"""Get the effective profile ID.
|
|
336
|
+
|
|
337
|
+
Returns the profile ID from the AMAZON_AD_API_PROFILE_ID
|
|
338
|
+
environment variable.
|
|
339
|
+
|
|
340
|
+
:return: Profile ID or None
|
|
341
|
+
:rtype: Optional[str]
|
|
342
|
+
"""
|
|
343
|
+
return self.ad_api_profile_id
|
|
344
|
+
|
|
345
|
+
@property
|
|
346
|
+
def region_endpoint(self) -> str:
|
|
347
|
+
"""Get the region-specific endpoint.
|
|
348
|
+
|
|
349
|
+
Returns the appropriate Amazon Ads API endpoint based on the
|
|
350
|
+
configured region and sandbox mode setting.
|
|
351
|
+
|
|
352
|
+
:return: Region-specific API endpoint URL
|
|
353
|
+
:rtype: str
|
|
354
|
+
"""
|
|
355
|
+
base = RegionConfig.get_api_endpoint(self.amazon_ads_region)
|
|
356
|
+
if self.amazon_ads_sandbox_mode:
|
|
357
|
+
base = base.replace("advertising-api", "advertising-api-test")
|
|
358
|
+
return base
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
settings = Settings()
|
|
362
|
+
"""Global settings instance for the Amazon Ads MCP server.
|
|
363
|
+
|
|
364
|
+
This instance is created once and used throughout the application
|
|
365
|
+
to access configuration settings.
|
|
366
|
+
"""
|