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,11 @@
1
+ """Amazon Ads API MCP Server package.
2
+
3
+ This package provides a Model Context Protocol (MCP) server for interacting
4
+ with the Amazon Advertising API. It includes authentication management,
5
+ identity handling, and dynamic OpenAPI integration for seamless API access.
6
+
7
+ :var __version__: Current package version
8
+ :type __version__: str
9
+ """
10
+
11
+ __version__ = "0.2.7"
@@ -0,0 +1,33 @@
1
+ """Authentication module for Amazon Ads MCP.
2
+
3
+ This module provides authentication management for the Amazon Ads MCP server,
4
+ including identity management, token handling, and authentication providers.
5
+ Uses a pluggable provider architecture supporting multiple authentication methods.
6
+
7
+ :var __all__: List of public exports from this module
8
+ :type __all__: List[str]
9
+ """
10
+
11
+ # Import providers to trigger registration
12
+ from . import ( # noqa: F401 # imported for side effects (provider registration)
13
+ providers,
14
+ )
15
+ from .base import (
16
+ BaseAmazonAdsProvider,
17
+ BaseAuthProvider,
18
+ BaseIdentityProvider,
19
+ ProviderConfig,
20
+ )
21
+ from .manager import AuthManager, get_auth_manager
22
+ from .registry import ProviderRegistry, register_provider
23
+
24
+ __all__ = [
25
+ "AuthManager",
26
+ "get_auth_manager",
27
+ "ProviderRegistry",
28
+ "register_provider",
29
+ "BaseAuthProvider",
30
+ "BaseIdentityProvider",
31
+ "BaseAmazonAdsProvider",
32
+ "ProviderConfig",
33
+ ]
@@ -0,0 +1,211 @@
1
+ """Define base authentication provider interfaces.
2
+
3
+ Provide abstract contracts for authentication providers and Amazon Ads
4
+ specializations, enabling pluggable auth mechanisms (e.g., direct OAuth,
5
+ OpenBridge) with consistent capabilities and region handling.
6
+
7
+ Examples
8
+ --------
9
+ See the BaseAmazonAdsProvider class for a complete example of how to
10
+ implement a custom authentication provider.
11
+ """
12
+
13
+ from abc import ABC, abstractmethod
14
+ from typing import Any, Dict, List, Optional
15
+
16
+ from ..models import AuthCredentials, Identity, Token
17
+ from ..utils.region_config import RegionConfig
18
+
19
+
20
+ class BaseAuthProvider(ABC):
21
+ """Provide the core authentication interface.
22
+
23
+ Define the minimal contract for all authentication providers regardless
24
+ of mechanism (OAuth2, API key, etc.).
25
+ """
26
+
27
+ @property
28
+ @abstractmethod
29
+ def provider_type(self) -> str:
30
+ """Return the provider type identifier.
31
+
32
+ :return: Provider type (e.g., "openbridge", "direct").
33
+ """
34
+ pass
35
+
36
+ @abstractmethod
37
+ async def initialize(self) -> None:
38
+ """Initialize the provider.
39
+
40
+ Perform required setup (e.g., configuration validation).
41
+ """
42
+ pass
43
+
44
+ @abstractmethod
45
+ async def get_token(self) -> Token:
46
+ """Return a valid authentication token.
47
+
48
+ Refresh the token if necessary.
49
+
50
+ :return: Valid authentication token.
51
+ """
52
+ pass
53
+
54
+ @abstractmethod
55
+ async def validate_token(self, token: Token) -> bool:
56
+ """Return whether the token is still valid.
57
+
58
+ :param token: Token to validate.
59
+ :return: True if token is valid, False otherwise.
60
+ """
61
+ pass
62
+
63
+ @abstractmethod
64
+ async def close(self) -> None:
65
+ """Clean up provider resources."""
66
+ pass
67
+
68
+
69
+ class BaseIdentityProvider(ABC):
70
+ """Provide multi-identity capabilities for providers.
71
+
72
+ Implementers can list identities, resolve an identity, and obtain
73
+ identity-scoped credentials for downstream API calls.
74
+ """
75
+
76
+ @abstractmethod
77
+ async def list_identities(self, **kwargs) -> List[Identity]:
78
+ """List available identities.
79
+
80
+ :param kwargs: Optional provider-specific filters.
81
+ :return: List of identities.
82
+ """
83
+ pass
84
+
85
+ @abstractmethod
86
+ async def get_identity(self, identity_id: str) -> Optional[Identity]:
87
+ """Return a specific identity by identifier.
88
+
89
+ :param identity_id: Identity identifier.
90
+ :return: Identity if found, otherwise None.
91
+ """
92
+ pass
93
+
94
+ @abstractmethod
95
+ async def get_identity_credentials(self, identity_id: str) -> AuthCredentials:
96
+ """Return credentials for a specific identity.
97
+
98
+ :param identity_id: Identity identifier.
99
+ :return: Authentication credentials for the identity.
100
+ """
101
+ pass
102
+
103
+
104
+ class BaseAmazonAdsProvider(BaseAuthProvider):
105
+ """Provide Amazon Ads–specific provider functionality.
106
+
107
+ Add region handling, header generation, and endpoint resolution for
108
+ Amazon Advertising API integrations.
109
+ """
110
+
111
+ @property
112
+ @abstractmethod
113
+ def region(self) -> str:
114
+ """Return the current region code ("na", "eu", or "fe")."""
115
+ pass
116
+
117
+ @abstractmethod
118
+ async def get_headers(self) -> Dict[str, str]:
119
+ """Return authentication headers for Amazon Ads API requests.
120
+
121
+ The returned mapping can include Authorization, ClientId, and
122
+ optional scope headers as required by the downstream API.
123
+
124
+ :return: Header mapping for requests.
125
+ """
126
+ pass
127
+
128
+ def get_region_endpoint(self, region: str = None) -> str:
129
+ """Return the API endpoint for the given region.
130
+
131
+ :param region: Region code; defaults to the provider's region.
132
+ :return: API endpoint URL.
133
+ """
134
+ region = region or self.region
135
+ return RegionConfig.get_api_endpoint(region)
136
+
137
+ def requires_identity_region_routing(self) -> bool:
138
+ """Return whether requests must target the identity's region.
139
+
140
+ For providers that manage identities across regions, this indicates
141
+ whether API requests should always be routed to the region associated
142
+ with the active identity.
143
+
144
+ :return: True if identity-based region routing is required.
145
+ """
146
+ return False
147
+
148
+ def headers_are_identity_specific(self) -> bool:
149
+ """Return whether authentication headers vary per identity.
150
+
151
+ For providers where different identities require different headers
152
+ (e.g., different client IDs or tokens), this indicates headers
153
+ cannot be reconstructed from a cached token alone.
154
+
155
+ :return: True if headers are identity specific.
156
+ """
157
+ return False
158
+
159
+ def region_controlled_by_identity(self) -> bool:
160
+ """Return whether the region is determined by the active identity.
161
+
162
+ For providers that bind region to identity, region changes require
163
+ selecting an identity in the target region.
164
+
165
+ :return: True if region is controlled by identity.
166
+ """
167
+ return False
168
+
169
+ def get_oauth_endpoint(self, region: str = None) -> str:
170
+ """Return the OAuth endpoint for the given region.
171
+
172
+ :param region: Region code; defaults to the provider's region.
173
+ :return: OAuth endpoint URL.
174
+ """
175
+ region = region or self.region
176
+ return RegionConfig.get_oauth_endpoint(region)
177
+
178
+
179
+ class ProviderConfig:
180
+ """Hold provider configuration values.
181
+
182
+ Store arbitrary configuration for providers with both mapping-style and
183
+ attribute-style access for convenience.
184
+ """
185
+
186
+ def __init__(self, **kwargs):
187
+ """Initialize configuration from keyword arguments.
188
+
189
+ :param kwargs: Provider-specific configuration parameters.
190
+ """
191
+ self._config = kwargs
192
+
193
+ def get(self, key: str, default: Any = None) -> Any:
194
+ """Return configuration value by key.
195
+
196
+ :param key: Configuration key to retrieve.
197
+ :param default: Default value if key not present.
198
+ :return: The configuration value or the default.
199
+ """
200
+ return self._config.get(key, default)
201
+
202
+ def __getattr__(self, name: str) -> Any:
203
+ """Provide attribute-style access to configuration values.
204
+
205
+ :param name: Attribute name to access.
206
+ :return: The configuration value.
207
+ :raises AttributeError: If the attribute is not defined.
208
+ """
209
+ if name in self._config:
210
+ return self._config[name]
211
+ raise AttributeError(f"Config has no attribute '{name}'")
@@ -0,0 +1,172 @@
1
+ """FastMCP 2.11 authentication hooks for request authentication.
2
+
3
+ This module provides request hooks that integrate with FastMCP 2.11's
4
+ hook system to automatically add authentication headers to outgoing
5
+ HTTP requests, replacing the need for custom HTTP clients.
6
+
7
+ The module provides:
8
+ - AuthHeaderHook: Automatic authentication header injection
9
+ - Header pollution cleanup for MCP client compatibility
10
+ - Special endpoint handling (e.g., profiles endpoint)
11
+ - Response processing for auth-related errors
12
+ """
13
+
14
+ import logging
15
+ from typing import Optional
16
+
17
+ import httpx
18
+ from fastmcp import Context
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class AuthHeaderHook:
24
+ """Request hook that adds authentication headers to outgoing requests.
25
+
26
+ This hook integrates with FastMCP 2.11's request lifecycle to automatically
27
+ inject authentication headers into all outgoing HTTP requests. It replaces
28
+ the need for a custom AuthenticatedClient by working with FastMCP's native
29
+ HTTP client.
30
+
31
+ The hook provides:
32
+ - Automatic authentication header injection
33
+ - Header pollution cleanup for MCP client compatibility
34
+ - Special endpoint handling (e.g., profiles endpoint)
35
+ - Response processing for auth-related errors
36
+
37
+ Example:
38
+ >>> auth_hook = AuthHeaderHook(auth_manager)
39
+ >>> base_mcp.add_request_hook(auth_hook)
40
+ """
41
+
42
+ def __init__(self, auth_manager):
43
+ """Initialize the authentication header hook.
44
+
45
+ Sets up the hook with the authentication manager that will
46
+ provide the necessary headers for requests.
47
+
48
+ :param auth_manager: The authentication manager instance that provides headers
49
+ :type auth_manager: AuthManager
50
+ :return: None
51
+ :rtype: None
52
+ """
53
+ self.auth_manager = auth_manager
54
+
55
+ async def before_request(
56
+ self, request: httpx.Request, ctx: Optional[Context] = None
57
+ ) -> httpx.Request:
58
+ """Add authentication headers before the request is sent.
59
+
60
+ This method is called by FastMCP before each HTTP request is sent.
61
+ It retrieves the current authentication headers from the auth manager
62
+ and adds them to the request, while also cleaning up any polluted
63
+ headers that might interfere with the Amazon Ads API.
64
+
65
+ :param request: The HTTP request to modify
66
+ :type request: httpx.Request
67
+ :param ctx: Optional FastMCP context for the current operation
68
+ :type ctx: Optional[Context]
69
+ :return: The modified request with authentication headers added
70
+ :rtype: httpx.Request
71
+ """
72
+ if not self.auth_manager:
73
+ return request
74
+
75
+ try:
76
+ # Get auth headers from manager
77
+ auth_headers = await self.auth_manager.get_headers()
78
+ logger.debug(f"Got auth headers: {list(auth_headers.keys())}")
79
+ except Exception as e:
80
+ logger.error(f"Failed to get auth headers: {e}")
81
+ return request
82
+
83
+ # Clean polluted MCP client headers
84
+ # FastMCP sometimes forwards headers from the MCP client that
85
+ # shouldn't go to the Amazon Ads API
86
+ polluted_headers = []
87
+ for key in list(request.headers.keys()):
88
+ key_lower = key.lower()
89
+ if any(
90
+ pattern in key_lower
91
+ for pattern in [
92
+ "authorization",
93
+ "clientid",
94
+ "client-id",
95
+ "client_id",
96
+ "amazon-ads",
97
+ "amazon-advertising",
98
+ "scope",
99
+ ]
100
+ ):
101
+ polluted_headers.append(key)
102
+ del request.headers[key]
103
+
104
+ if polluted_headers:
105
+ logger.debug(
106
+ f"Removed {len(polluted_headers)} polluted headers: {polluted_headers}"
107
+ )
108
+
109
+ # Check for special endpoint handling
110
+ url = str(request.url)
111
+
112
+ # Handle profiles endpoint special case
113
+ # The /v2/profiles endpoint when listing (no profileId in URL)
114
+ # doesn't accept the Scope header
115
+ if "/v2/profiles" in url and "profileId" not in url:
116
+ auth_headers = auth_headers.copy()
117
+ auth_headers.pop("Amazon-Advertising-API-Scope", None)
118
+ logger.debug("Removed Scope header for profiles listing endpoint")
119
+
120
+ # Add auth headers to request
121
+ request.headers.update(auth_headers)
122
+
123
+ # Log final headers at DEBUG level only for production safety
124
+ if logger.isEnabledFor(logging.DEBUG):
125
+ logger.debug("Final headers being sent to Amazon:")
126
+ for key, value in request.headers.items():
127
+ if "authorization" in key.lower():
128
+ logger.debug(f" {key}: {value[:50]}...")
129
+ else:
130
+ logger.debug(f" {key}: {value}")
131
+
132
+ return request
133
+
134
+ async def after_response(
135
+ self, response: httpx.Response, ctx: Optional[Context] = None
136
+ ) -> httpx.Response:
137
+ """Process response after it's received.
138
+
139
+ This method can be used to handle auth-related response processing,
140
+ such as detecting expired tokens or rate limits.
141
+
142
+ :param response: The HTTP response received
143
+ :type response: httpx.Response
144
+ :param ctx: Optional FastMCP context for the current operation
145
+ :type ctx: Optional[Context]
146
+ :return: The response, potentially modified
147
+ :rtype: httpx.Response
148
+ """
149
+ # Log auth-related errors with more detail
150
+ if response.status_code == 401:
151
+ error_detail = ""
152
+ try:
153
+ error_body = response.json()
154
+ error_detail = f" - Error: {error_body}"
155
+ except Exception:
156
+ error_detail = f" - Response: {response.text[:200]}"
157
+
158
+ logger.error(f"Received 401 Unauthorized - token may be expired or invalid{error_detail}")
159
+ logger.error(f"Request URL: {response.request.url}")
160
+ logger.error(f"Request had headers: {list(response.request.headers.keys())}")
161
+
162
+ # Check for specific auth headers
163
+ auth_header = response.request.headers.get("authorization", "")
164
+ if not auth_header:
165
+ logger.error("CRITICAL: No Authorization header in request!")
166
+ elif not auth_header.startswith("Bearer "):
167
+ logger.error(f"CRITICAL: Authorization header missing 'Bearer ' prefix: {auth_header[:20]}...")
168
+
169
+ elif response.status_code == 403:
170
+ logger.warning("Received 403 Forbidden - check permissions")
171
+
172
+ return response