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,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")
|