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,156 @@
1
+ """Helper functions for sampling with automatic fallback."""
2
+
3
+ import logging
4
+ from typing import List, Optional, Union
5
+
6
+ from fastmcp import Context
7
+ from mcp.types import (
8
+ ContentBlock,
9
+ CreateMessageRequestParams,
10
+ SamplingMessage,
11
+ TextContent,
12
+ )
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ async def sample_with_fallback(
18
+ ctx: Context,
19
+ messages: Union[str, List[Union[str, SamplingMessage]]],
20
+ system_prompt: Optional[str] = None,
21
+ temperature: Optional[float] = None,
22
+ max_tokens: Optional[int] = None,
23
+ model_preferences: Optional[Union[str, List[str]]] = None,
24
+ ) -> ContentBlock:
25
+ """
26
+ Attempt to sample from the client with automatic server-side fallback.
27
+
28
+ This function first tries to use the client's sampling capability.
29
+ If the client doesn't support sampling and a server-side handler is available,
30
+ it automatically falls back to server-side sampling.
31
+
32
+ Args:
33
+ ctx: The FastMCP context
34
+ messages: String or list of messages to send
35
+ system_prompt: Optional system prompt
36
+ temperature: Optional sampling temperature
37
+ max_tokens: Optional max tokens (default 512)
38
+ model_preferences: Optional model preferences
39
+
40
+ Returns:
41
+ ContentBlock with the sampling result
42
+
43
+ Raises:
44
+ Exception: If neither client nor server sampling is available
45
+ """
46
+ # First try client-side sampling
47
+ try:
48
+ result = await ctx.sample(
49
+ messages=messages,
50
+ system_prompt=system_prompt,
51
+ temperature=temperature,
52
+ max_tokens=max_tokens or 512,
53
+ model_preferences=model_preferences,
54
+ )
55
+ logger.debug("Sampling completed via client")
56
+ return result
57
+
58
+ except Exception as client_error:
59
+ error_msg = str(client_error).lower()
60
+
61
+ # Check if it's a "client doesn't support sampling" error
62
+ if (
63
+ "does not support sampling" in error_msg
64
+ or "sampling not supported" in error_msg
65
+ ):
66
+ # Try to get the fallback handler from context
67
+ # Only use public API methods
68
+ fallback_handler = None
69
+ if hasattr(ctx, "get_sampling_handler"):
70
+ fallback_handler = ctx.get_sampling_handler()
71
+
72
+ # Log what we found for debugging
73
+ logger.debug(
74
+ f"Looking for fallback handler: ctx has handler={fallback_handler is not None}"
75
+ )
76
+
77
+ if not fallback_handler:
78
+ # Try to get from sampling wrapper if available
79
+ try:
80
+ from .sampling_wrapper import get_sampling_wrapper
81
+
82
+ wrapper = get_sampling_wrapper()
83
+ if wrapper.has_handler():
84
+ fallback_handler = wrapper
85
+ logger.debug(
86
+ f"Found handler from sampling wrapper: {fallback_handler is not None}"
87
+ )
88
+ except Exception as e:
89
+ logger.debug(f"Could not get from sampling wrapper: {e}")
90
+
91
+ if fallback_handler:
92
+ logger.info(
93
+ "Client doesn't support sampling, using server-side fallback"
94
+ )
95
+
96
+ # Convert messages to SamplingMessage format if needed
97
+ if isinstance(messages, str):
98
+ sampling_messages = [
99
+ SamplingMessage(
100
+ role="user",
101
+ content=TextContent(type="text", text=messages),
102
+ )
103
+ ]
104
+ elif isinstance(messages, list):
105
+ sampling_messages = []
106
+ for msg in messages:
107
+ if isinstance(msg, str):
108
+ sampling_messages.append(
109
+ SamplingMessage(
110
+ role="user",
111
+ content=TextContent(type="text", text=msg),
112
+ )
113
+ )
114
+ else:
115
+ sampling_messages.append(msg)
116
+ else:
117
+ sampling_messages = messages
118
+
119
+ # Create request parameters
120
+ params = CreateMessageRequestParams(
121
+ messages=sampling_messages,
122
+ systemPrompt=system_prompt,
123
+ temperature=temperature,
124
+ maxTokens=max_tokens or 512,
125
+ )
126
+
127
+ if model_preferences:
128
+ if isinstance(model_preferences, str):
129
+ params.modelPreferences = {
130
+ "hints": [{"name": model_preferences}]
131
+ }
132
+ elif isinstance(model_preferences, list):
133
+ params.modelPreferences = {
134
+ "hints": [{"name": m} for m in model_preferences]
135
+ }
136
+
137
+ # Call the fallback handler
138
+ result = await fallback_handler(
139
+ sampling_messages, params, ctx.request_context
140
+ )
141
+
142
+ # Extract content from result
143
+ if hasattr(result, "content"):
144
+ logger.debug("Sampling completed via server-side fallback")
145
+ return result.content
146
+ else:
147
+ return result
148
+ else:
149
+ logger.warning("No sampling fallback handler available")
150
+ raise Exception(
151
+ "Client does not support sampling and no server-side fallback is configured. "
152
+ "Set SAMPLING_ENABLED=true and provide OPENAI_API_KEY to enable server-side sampling."
153
+ )
154
+ else:
155
+ # Different error, re-raise
156
+ raise client_error
@@ -0,0 +1,173 @@
1
+ """Wrapper for sampling functionality without accessing private APIs."""
2
+
3
+ import logging
4
+ from typing import Any, Dict, List, Optional, Union
5
+
6
+ from fastmcp import Context
7
+ from mcp.types import CreateMessageRequestParams, SamplingMessage, TextContent
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class SamplingHandlerWrapper:
13
+ """
14
+ Wrapper for sampling handler that avoids accessing private FastMCP attributes.
15
+
16
+ This wrapper provides a clean interface to sampling functionality without
17
+ relying on private APIs or implementation details.
18
+ """
19
+
20
+ def __init__(self, sampling_handler=None):
21
+ """
22
+ Initialize the sampling wrapper.
23
+
24
+ Args:
25
+ sampling_handler: Optional sampling handler to wrap
26
+ """
27
+ self._handler = sampling_handler
28
+ self._fallback_configured = False
29
+
30
+ def set_handler(self, handler):
31
+ """Set the sampling handler."""
32
+ self._handler = handler
33
+ self._fallback_configured = True
34
+ logger.debug("Sampling handler configured")
35
+
36
+ def has_handler(self) -> bool:
37
+ """Check if a handler is configured."""
38
+ return self._handler is not None
39
+
40
+ async def sample(
41
+ self,
42
+ messages: Union[str, List[Any]],
43
+ ctx: Context,
44
+ system_prompt: Optional[str] = None,
45
+ temperature: float = 0.0,
46
+ max_tokens: Optional[int] = None,
47
+ model_preferences: Optional[Union[str, List[str]]] = None,
48
+ ) -> Any:
49
+ """
50
+ Sample with fallback handling.
51
+
52
+ This method provides sampling with proper fallback handling
53
+ without accessing private FastMCP attributes.
54
+
55
+ Args:
56
+ messages: Messages to sample
57
+ ctx: Context for the operation
58
+ system_prompt: Optional system prompt
59
+ temperature: Sampling temperature
60
+ max_tokens: Maximum tokens to generate
61
+ model_preferences: Model preferences
62
+
63
+ Returns:
64
+ Sampling result
65
+
66
+ Raises:
67
+ Exception: If no handler is available and client doesn't support sampling
68
+ """
69
+ # First try the client sampling
70
+ try:
71
+ result = await ctx.sample(
72
+ messages=messages,
73
+ system_prompt=system_prompt,
74
+ temperature=temperature,
75
+ max_tokens=max_tokens,
76
+ model_preferences=model_preferences,
77
+ )
78
+ logger.debug("Sampling completed via client")
79
+ return result
80
+ except Exception as client_error:
81
+ if "does not support sampling" not in str(client_error):
82
+ # Different error, re-raise
83
+ raise client_error
84
+
85
+ # Client doesn't support sampling, try fallback
86
+ if not self._handler:
87
+ logger.warning("No sampling fallback handler available")
88
+ raise Exception(
89
+ "Client does not support sampling and no server-side fallback is configured. "
90
+ "Set SAMPLING_ENABLED=true and provide OPENAI_API_KEY to enable server-side sampling."
91
+ )
92
+
93
+ logger.info("Client doesn't support sampling, using server-side fallback")
94
+
95
+ # Convert messages to SamplingMessage format
96
+ sampling_messages = self._convert_messages(messages)
97
+
98
+ # Create request parameters
99
+ params = CreateMessageRequestParams(
100
+ messages=sampling_messages,
101
+ systemPrompt=system_prompt,
102
+ temperature=temperature,
103
+ maxTokens=max_tokens or 512,
104
+ )
105
+
106
+ if model_preferences:
107
+ params.modelPreferences = self._convert_model_preferences(
108
+ model_preferences
109
+ )
110
+
111
+ # Call the handler
112
+ result = await self._handler(sampling_messages, params, ctx.request_context)
113
+
114
+ # Extract content from result
115
+ if hasattr(result, "content"):
116
+ logger.debug("Sampling completed via server-side fallback")
117
+ return result.content
118
+ else:
119
+ return result
120
+
121
+ def _convert_messages(
122
+ self, messages: Union[str, List[Any]]
123
+ ) -> List[SamplingMessage]:
124
+ """Convert messages to SamplingMessage format."""
125
+ if isinstance(messages, str):
126
+ return [
127
+ SamplingMessage(
128
+ role="user",
129
+ content=TextContent(type="text", text=messages),
130
+ )
131
+ ]
132
+ elif isinstance(messages, list):
133
+ sampling_messages = []
134
+ for msg in messages:
135
+ if isinstance(msg, str):
136
+ sampling_messages.append(
137
+ SamplingMessage(
138
+ role="user",
139
+ content=TextContent(type="text", text=msg),
140
+ )
141
+ )
142
+ else:
143
+ sampling_messages.append(msg)
144
+ return sampling_messages
145
+ else:
146
+ return messages
147
+
148
+ def _convert_model_preferences(self, preferences: Union[str, List[str]]) -> Dict:
149
+ """Convert model preferences to proper format."""
150
+ if isinstance(preferences, str):
151
+ return {"hints": [{"name": preferences}]}
152
+ elif isinstance(preferences, list):
153
+ return {"hints": [{"name": m} for m in preferences]}
154
+ return {}
155
+
156
+
157
+ # Global instance
158
+ _sampling_wrapper: Optional[SamplingHandlerWrapper] = None
159
+
160
+
161
+ def get_sampling_wrapper() -> SamplingHandlerWrapper:
162
+ """Get or create the global sampling wrapper."""
163
+ global _sampling_wrapper
164
+ if _sampling_wrapper is None:
165
+ _sampling_wrapper = SamplingHandlerWrapper()
166
+ return _sampling_wrapper
167
+
168
+
169
+ def configure_sampling_handler(handler):
170
+ """Configure the global sampling handler."""
171
+ wrapper = get_sampling_wrapper()
172
+ wrapper.set_handler(handler)
173
+ logger.info("Sampling handler configured via wrapper")