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,22 @@
|
|
|
1
|
+
"""Tools module for Amazon Ads MCP.
|
|
2
|
+
|
|
3
|
+
This module provides MCP tools for Amazon Ads API integration,
|
|
4
|
+
including identity management and API operation tools.
|
|
5
|
+
|
|
6
|
+
:var __all__: List of public exports from this module
|
|
7
|
+
:type __all__: List[str]
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .identity import (
|
|
11
|
+
get_active_identity,
|
|
12
|
+
get_identity_info,
|
|
13
|
+
list_remote_identities,
|
|
14
|
+
set_active_identity,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"list_remote_identities",
|
|
19
|
+
"get_active_identity",
|
|
20
|
+
"set_active_identity",
|
|
21
|
+
"get_identity_info",
|
|
22
|
+
]
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Cache management tools for MCP server.
|
|
2
|
+
|
|
3
|
+
This module provides tools for managing and clearing caches
|
|
4
|
+
to ensure fresh data is fetched from the API.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Dict
|
|
9
|
+
|
|
10
|
+
from ..auth.manager import get_auth_manager
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def clear_identity_cache() -> Dict:
|
|
16
|
+
"""Clear the OpenBridge identity cache to force fresh fetch.
|
|
17
|
+
|
|
18
|
+
This function clears the cached identities in the OpenBridge provider
|
|
19
|
+
to ensure the next call to list_identities fetches fresh data from
|
|
20
|
+
the API instead of returning cached results.
|
|
21
|
+
|
|
22
|
+
:return: Status of the cache clearing operation
|
|
23
|
+
:rtype: Dict
|
|
24
|
+
"""
|
|
25
|
+
try:
|
|
26
|
+
auth_manager = get_auth_manager()
|
|
27
|
+
|
|
28
|
+
# Check if provider has cache
|
|
29
|
+
if hasattr(auth_manager.provider, "_identities_cache"):
|
|
30
|
+
cache_size = len(auth_manager.provider._identities_cache)
|
|
31
|
+
auth_manager.provider._identities_cache.clear()
|
|
32
|
+
logger.info(f"Cleared identity cache ({cache_size} entries)")
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
"success": True,
|
|
36
|
+
"message": f"Identity cache cleared ({cache_size} entries removed)",
|
|
37
|
+
"cache_size_before": cache_size,
|
|
38
|
+
"cache_size_after": 0,
|
|
39
|
+
}
|
|
40
|
+
else:
|
|
41
|
+
return {
|
|
42
|
+
"success": True,
|
|
43
|
+
"message": "No identity cache found (provider may not use caching)",
|
|
44
|
+
"cache_size_before": 0,
|
|
45
|
+
"cache_size_after": 0,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
except Exception as e:
|
|
49
|
+
logger.error(f"Failed to clear identity cache: {e}")
|
|
50
|
+
return {
|
|
51
|
+
"success": False,
|
|
52
|
+
"error": str(e),
|
|
53
|
+
"message": "Failed to clear identity cache",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def get_cache_status() -> Dict:
|
|
58
|
+
"""Get the current status of the identity cache.
|
|
59
|
+
|
|
60
|
+
Returns information about the current cache state including
|
|
61
|
+
the number of cached entries and their keys.
|
|
62
|
+
|
|
63
|
+
:return: Cache status information
|
|
64
|
+
:rtype: Dict
|
|
65
|
+
"""
|
|
66
|
+
try:
|
|
67
|
+
auth_manager = get_auth_manager()
|
|
68
|
+
|
|
69
|
+
if hasattr(auth_manager.provider, "_identities_cache"):
|
|
70
|
+
cache = auth_manager.provider._identities_cache
|
|
71
|
+
cache_keys = list(cache.keys())
|
|
72
|
+
cache_size = len(cache)
|
|
73
|
+
|
|
74
|
+
# Count identities in cache
|
|
75
|
+
total_identities = 0
|
|
76
|
+
cache_details = []
|
|
77
|
+
for key in cache_keys:
|
|
78
|
+
identities = cache[key]
|
|
79
|
+
total_identities += len(identities)
|
|
80
|
+
cache_details.append(
|
|
81
|
+
{"key": str(key), "identity_count": len(identities)}
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
"success": True,
|
|
86
|
+
"cache_enabled": True,
|
|
87
|
+
"cache_size": cache_size,
|
|
88
|
+
"total_identities_cached": total_identities,
|
|
89
|
+
"cache_entries": cache_details,
|
|
90
|
+
"message": f"Cache contains {cache_size} entries with {total_identities} total identities",
|
|
91
|
+
}
|
|
92
|
+
else:
|
|
93
|
+
return {
|
|
94
|
+
"success": True,
|
|
95
|
+
"cache_enabled": False,
|
|
96
|
+
"message": "No identity cache found",
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
except Exception as e:
|
|
100
|
+
logger.error(f"Failed to get cache status: {e}")
|
|
101
|
+
return {
|
|
102
|
+
"success": False,
|
|
103
|
+
"error": str(e),
|
|
104
|
+
"message": "Failed to get cache status",
|
|
105
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""Download management tools for Amazon Ads MCP server.
|
|
2
|
+
|
|
3
|
+
This module provides tools for managing downloads of exports and reports
|
|
4
|
+
from the Amazon Ads API. It handles export status checking, file
|
|
5
|
+
downloading, local file management, and cleanup operations.
|
|
6
|
+
|
|
7
|
+
The tools integrate with the export download handler to provide a
|
|
8
|
+
unified interface for managing downloaded data files.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict, Optional
|
|
15
|
+
|
|
16
|
+
from ..utils.export_download_handler import get_download_handler
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def check_and_download_export(
|
|
22
|
+
export_id: str,
|
|
23
|
+
export_response: Dict[str, Any],
|
|
24
|
+
export_type: Optional[str] = None,
|
|
25
|
+
profile_id: Optional[str] = None,
|
|
26
|
+
) -> Dict[str, Any]:
|
|
27
|
+
"""Check export status and download if ready.
|
|
28
|
+
|
|
29
|
+
This function checks the status of an export and attempts to download
|
|
30
|
+
it if it's ready. It can infer the export type from the export ID
|
|
31
|
+
if not provided, and handles various export statuses appropriately.
|
|
32
|
+
|
|
33
|
+
:param export_id: The export ID to check and download
|
|
34
|
+
:type export_id: str
|
|
35
|
+
:param export_response: Response from the GetExport API call
|
|
36
|
+
:type export_response: Dict[str, Any]
|
|
37
|
+
:param export_type: Optional export type (campaign, adgroup, etc.)
|
|
38
|
+
:type export_type: Optional[str]
|
|
39
|
+
:param profile_id: Optional profile ID for scoped storage
|
|
40
|
+
:type profile_id: Optional[str]
|
|
41
|
+
:return: Dictionary containing status and download information
|
|
42
|
+
:rtype: Dict[str, Any]
|
|
43
|
+
"""
|
|
44
|
+
handler = get_download_handler()
|
|
45
|
+
|
|
46
|
+
# Infer export type from response if not provided
|
|
47
|
+
if not export_type and export_id:
|
|
48
|
+
# Try to decode from export ID
|
|
49
|
+
import base64
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
padded = export_id + "=" * (4 - len(export_id) % 4)
|
|
53
|
+
decoded = base64.b64decode(padded).decode("utf-8")
|
|
54
|
+
if "," in decoded:
|
|
55
|
+
_, suffix = decoded.rsplit(",", 1)
|
|
56
|
+
type_map = {
|
|
57
|
+
"C": "campaigns",
|
|
58
|
+
"A": "adgroups",
|
|
59
|
+
"AD": "ads",
|
|
60
|
+
"T": "targets",
|
|
61
|
+
}
|
|
62
|
+
export_type = type_map.get(suffix.upper(), "general")
|
|
63
|
+
except (AttributeError, TypeError, ValueError, KeyError):
|
|
64
|
+
export_type = "general"
|
|
65
|
+
|
|
66
|
+
# Handle the export response with profile scoping
|
|
67
|
+
file_path = await handler.handle_export_response(
|
|
68
|
+
export_response, export_type, profile_id=profile_id
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if file_path:
|
|
72
|
+
return {
|
|
73
|
+
"success": True,
|
|
74
|
+
"status": "downloaded",
|
|
75
|
+
"file_path": str(file_path),
|
|
76
|
+
"export_id": export_id,
|
|
77
|
+
"message": f"Export downloaded successfully to {file_path}",
|
|
78
|
+
}
|
|
79
|
+
else:
|
|
80
|
+
status = export_response.get("status", "UNKNOWN")
|
|
81
|
+
if status == "PROCESSING":
|
|
82
|
+
return {
|
|
83
|
+
"success": False,
|
|
84
|
+
"status": "processing",
|
|
85
|
+
"export_id": export_id,
|
|
86
|
+
"message": "Export is still being processed. Check again later.",
|
|
87
|
+
}
|
|
88
|
+
elif status == "FAILED":
|
|
89
|
+
error = export_response.get("error", {})
|
|
90
|
+
return {
|
|
91
|
+
"success": False,
|
|
92
|
+
"status": "failed",
|
|
93
|
+
"export_id": export_id,
|
|
94
|
+
"error": error,
|
|
95
|
+
"message": f"Export failed: {error.get('message', 'Unknown error')}",
|
|
96
|
+
}
|
|
97
|
+
else:
|
|
98
|
+
return {
|
|
99
|
+
"success": False,
|
|
100
|
+
"status": status.lower(),
|
|
101
|
+
"export_id": export_id,
|
|
102
|
+
"message": f"Export has status {status} - no download available",
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
async def list_downloaded_files(
|
|
107
|
+
resource_type: Optional[str] = None,
|
|
108
|
+
profile_id: Optional[str] = None,
|
|
109
|
+
) -> Dict[str, Any]:
|
|
110
|
+
"""List all downloaded files in the data directory.
|
|
111
|
+
|
|
112
|
+
Scans the data directory for downloaded files and provides a summary
|
|
113
|
+
of all available downloads. Can filter by resource type to show only
|
|
114
|
+
specific types of downloads.
|
|
115
|
+
|
|
116
|
+
When profile_id is provided:
|
|
117
|
+
- Lists files only from data/profiles/{profile_id}/
|
|
118
|
+
|
|
119
|
+
When profile_id is None:
|
|
120
|
+
- Lists files only from legacy (non-profile) directories
|
|
121
|
+
|
|
122
|
+
:param resource_type: Optional filter to show only specific resource types
|
|
123
|
+
:type resource_type: Optional[str]
|
|
124
|
+
:param profile_id: Optional profile ID for scoped listing
|
|
125
|
+
:type profile_id: Optional[str]
|
|
126
|
+
:return: Dictionary containing download summary and file listings
|
|
127
|
+
:rtype: Dict[str, Any]
|
|
128
|
+
"""
|
|
129
|
+
handler = get_download_handler()
|
|
130
|
+
# handler.list_downloads returns a flat list of file dicts
|
|
131
|
+
files = handler.list_downloads(resource_type, profile_id=profile_id)
|
|
132
|
+
|
|
133
|
+
# Calculate totals from the flat list
|
|
134
|
+
total_files = len(files)
|
|
135
|
+
total_size = sum(f.get("size", 0) for f in files)
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
"base_directory": str(handler.base_dir),
|
|
139
|
+
"total_files": total_files,
|
|
140
|
+
"total_size_bytes": total_size,
|
|
141
|
+
"files": files, # Flat list format
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
async def get_download_metadata(file_path: str) -> Dict[str, Any]:
|
|
146
|
+
"""Get metadata for a downloaded file.
|
|
147
|
+
|
|
148
|
+
Retrieves metadata associated with a downloaded file, including
|
|
149
|
+
any custom metadata stored alongside the file. Falls back to
|
|
150
|
+
basic file information if no metadata is available.
|
|
151
|
+
|
|
152
|
+
:param file_path: Path to the downloaded file
|
|
153
|
+
:type file_path: str
|
|
154
|
+
:return: Dictionary containing file metadata and status
|
|
155
|
+
:rtype: Dict[str, Any]
|
|
156
|
+
"""
|
|
157
|
+
path = Path(file_path)
|
|
158
|
+
|
|
159
|
+
if not path.exists():
|
|
160
|
+
return {
|
|
161
|
+
"success": False,
|
|
162
|
+
"error": "File not found",
|
|
163
|
+
"file_path": file_path,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
meta_path = path.with_suffix(".meta.json")
|
|
167
|
+
|
|
168
|
+
if meta_path.exists():
|
|
169
|
+
with open(meta_path) as f:
|
|
170
|
+
metadata = json.load(f)
|
|
171
|
+
return {"success": True, "file_path": file_path, "metadata": metadata}
|
|
172
|
+
else:
|
|
173
|
+
# Return basic file info
|
|
174
|
+
stat = path.stat()
|
|
175
|
+
return {
|
|
176
|
+
"success": True,
|
|
177
|
+
"file_path": file_path,
|
|
178
|
+
"metadata": {
|
|
179
|
+
"file_name": path.name,
|
|
180
|
+
"file_size": stat.st_size,
|
|
181
|
+
"modified": stat.st_mtime,
|
|
182
|
+
"note": "No detailed metadata available",
|
|
183
|
+
},
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
async def clean_old_downloads(
|
|
188
|
+
profile_id: str,
|
|
189
|
+
resource_type: Optional[str] = None,
|
|
190
|
+
days_old: int = 7,
|
|
191
|
+
) -> Dict[str, Any]:
|
|
192
|
+
"""Clean up old downloaded files for a specific profile.
|
|
193
|
+
|
|
194
|
+
Removes downloaded files that are older than the specified number
|
|
195
|
+
of days within the profile's storage directory. This helps manage
|
|
196
|
+
disk space by cleaning up outdated export data. Can filter by
|
|
197
|
+
resource type to clean only specific types of downloads.
|
|
198
|
+
|
|
199
|
+
IMPORTANT: This function is profile-scoped to maintain multi-tenant
|
|
200
|
+
isolation. It only deletes files within data/profiles/{profile_id}/.
|
|
201
|
+
|
|
202
|
+
:param profile_id: Profile ID for scoped cleanup (required)
|
|
203
|
+
:type profile_id: str
|
|
204
|
+
:param resource_type: Optional filter by resource type
|
|
205
|
+
:type resource_type: Optional[str]
|
|
206
|
+
:param days_old: Delete files older than this many days
|
|
207
|
+
:type days_old: int
|
|
208
|
+
:return: Dictionary containing cleanup summary and deleted file list
|
|
209
|
+
:rtype: Dict[str, Any]
|
|
210
|
+
"""
|
|
211
|
+
from datetime import datetime, timedelta
|
|
212
|
+
|
|
213
|
+
if not profile_id:
|
|
214
|
+
return {
|
|
215
|
+
"success": False,
|
|
216
|
+
"error": "profile_id is required for cleanup operations",
|
|
217
|
+
"deleted_files": 0,
|
|
218
|
+
"deleted_size_bytes": 0,
|
|
219
|
+
"files": [],
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
handler = get_download_handler()
|
|
223
|
+
cutoff_date = datetime.now() - timedelta(days=days_old)
|
|
224
|
+
|
|
225
|
+
# Use profile-scoped base directory for multi-tenant isolation
|
|
226
|
+
profile_base = handler.get_profile_base_dir(profile_id)
|
|
227
|
+
|
|
228
|
+
deleted_files = []
|
|
229
|
+
deleted_size = 0
|
|
230
|
+
|
|
231
|
+
if resource_type:
|
|
232
|
+
search_paths = [profile_base / resource_type]
|
|
233
|
+
else:
|
|
234
|
+
# Only search directories within the profile's storage
|
|
235
|
+
search_paths = [
|
|
236
|
+
p for p in profile_base.iterdir() if p.is_dir()
|
|
237
|
+
] if profile_base.exists() else []
|
|
238
|
+
|
|
239
|
+
for resource_dir in search_paths:
|
|
240
|
+
if not resource_dir.exists():
|
|
241
|
+
continue
|
|
242
|
+
|
|
243
|
+
for sub_dir in resource_dir.iterdir():
|
|
244
|
+
if sub_dir.is_dir():
|
|
245
|
+
for file_path in sub_dir.iterdir():
|
|
246
|
+
if file_path.is_file():
|
|
247
|
+
stat = file_path.stat()
|
|
248
|
+
modified = datetime.fromtimestamp(stat.st_mtime)
|
|
249
|
+
|
|
250
|
+
if modified < cutoff_date:
|
|
251
|
+
deleted_size += stat.st_size
|
|
252
|
+
deleted_files.append(str(file_path))
|
|
253
|
+
|
|
254
|
+
# Delete the file and its metadata
|
|
255
|
+
file_path.unlink()
|
|
256
|
+
meta_path = file_path.with_suffix(".meta.json")
|
|
257
|
+
if meta_path.exists():
|
|
258
|
+
meta_path.unlink()
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
"success": True,
|
|
262
|
+
"profile_id": profile_id,
|
|
263
|
+
"deleted_files": len(deleted_files),
|
|
264
|
+
"deleted_size_bytes": deleted_size,
|
|
265
|
+
"files": deleted_files,
|
|
266
|
+
"message": f"Deleted {len(deleted_files)} files older than {days_old} days for profile {profile_id}",
|
|
267
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""MCP tools for identity management.
|
|
2
|
+
|
|
3
|
+
This module provides MCP tools for managing remote identities
|
|
4
|
+
for Amazon Ads API access, including listing, selecting, and
|
|
5
|
+
querying identity information.
|
|
6
|
+
|
|
7
|
+
The tools provide:
|
|
8
|
+
|
|
9
|
+
- List all available remote identities from OpenBridge
|
|
10
|
+
- Set active identity for Amazon Ads API operations
|
|
11
|
+
- Get current active identity information
|
|
12
|
+
- Get detailed information about specific identities
|
|
13
|
+
- Comprehensive error handling and logging
|
|
14
|
+
|
|
15
|
+
Examples:
|
|
16
|
+
>>> identities = await list_remote_identities()
|
|
17
|
+
>>> active = await get_active_identity()
|
|
18
|
+
>>> response = await set_active_identity(
|
|
19
|
+
... SetActiveIdentityRequest(identity_id="123")
|
|
20
|
+
... )
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
from typing import Optional
|
|
25
|
+
|
|
26
|
+
from ..auth.manager import get_auth_manager
|
|
27
|
+
from ..models import (
|
|
28
|
+
Identity,
|
|
29
|
+
IdentityListResponse,
|
|
30
|
+
SetActiveIdentityRequest,
|
|
31
|
+
SetActiveIdentityResponse,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def list_remote_identities(
|
|
38
|
+
identity_type: Optional[str] = "14",
|
|
39
|
+
) -> IdentityListResponse:
|
|
40
|
+
"""List all available Amazon Ads remote identities.
|
|
41
|
+
|
|
42
|
+
Returns a list of remote identities that can be used to access
|
|
43
|
+
different Amazon Ads accounts through OpenBridge. By default, filters
|
|
44
|
+
for Amazon Ads identities (type 14). The function handles both
|
|
45
|
+
OpenBridge and direct authentication providers.
|
|
46
|
+
|
|
47
|
+
The function automatically determines the provider type and calls the
|
|
48
|
+
appropriate method for listing identities. For OpenBridge providers,
|
|
49
|
+
it passes the identity_type filter to get only relevant identities.
|
|
50
|
+
|
|
51
|
+
:param identity_type: Filter by remote identity type (default "14" for Amazon Ads)
|
|
52
|
+
:type identity_type: Optional[str]
|
|
53
|
+
:return: Response containing the list of available identities
|
|
54
|
+
:rtype: IdentityListResponse
|
|
55
|
+
:raises Exception: If listing identities fails
|
|
56
|
+
|
|
57
|
+
.. note::
|
|
58
|
+
Identity type "14" corresponds to Amazon Ads identities in OpenBridge.
|
|
59
|
+
|
|
60
|
+
.. example::
|
|
61
|
+
>>> response = await list_remote_identities()
|
|
62
|
+
>>> print(f"Found {response.total} identities")
|
|
63
|
+
"""
|
|
64
|
+
logger.info(f"Listing remote identities (type={identity_type})")
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
auth_manager = get_auth_manager()
|
|
68
|
+
# Pass the identity_type filter to the provider
|
|
69
|
+
if hasattr(auth_manager.provider, "list_identities"):
|
|
70
|
+
identities = await auth_manager.provider.list_identities(
|
|
71
|
+
identity_type=identity_type
|
|
72
|
+
)
|
|
73
|
+
else:
|
|
74
|
+
identities = await auth_manager.list_identities()
|
|
75
|
+
|
|
76
|
+
return IdentityListResponse(
|
|
77
|
+
identities=identities, total=len(identities), has_more=False
|
|
78
|
+
)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
logger.error(f"Failed to list identities: {e}")
|
|
81
|
+
raise
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def get_active_identity() -> Optional[Identity]:
|
|
85
|
+
"""Get the currently active remote identity.
|
|
86
|
+
|
|
87
|
+
Returns the identity that is currently being used for Amazon Ads API calls.
|
|
88
|
+
This function provides access to the currently selected identity
|
|
89
|
+
and logs information about it for debugging purposes.
|
|
90
|
+
|
|
91
|
+
The function retrieves the active identity from the authentication manager
|
|
92
|
+
and logs relevant information for troubleshooting.
|
|
93
|
+
|
|
94
|
+
:return: The active Identity, or None if no identity is set
|
|
95
|
+
:rtype: Optional[Identity]
|
|
96
|
+
|
|
97
|
+
.. example::
|
|
98
|
+
>>> identity = await get_active_identity()
|
|
99
|
+
>>> if identity:
|
|
100
|
+
... print(f"Active: {identity.attributes.get('name')}")
|
|
101
|
+
"""
|
|
102
|
+
logger.info("Getting active identity")
|
|
103
|
+
|
|
104
|
+
auth_manager = get_auth_manager()
|
|
105
|
+
active_identity = auth_manager.get_active_identity()
|
|
106
|
+
|
|
107
|
+
if active_identity:
|
|
108
|
+
name = active_identity.attributes.get("name", active_identity.id)
|
|
109
|
+
logger.info(f"Active identity: {name} ({active_identity.id})")
|
|
110
|
+
else:
|
|
111
|
+
logger.info("No active identity set")
|
|
112
|
+
|
|
113
|
+
return active_identity
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
async def set_active_identity(
|
|
117
|
+
request: SetActiveIdentityRequest,
|
|
118
|
+
) -> SetActiveIdentityResponse:
|
|
119
|
+
"""Set the active remote identity for Amazon Ads API operations.
|
|
120
|
+
|
|
121
|
+
This identity will be used for all subsequent Amazon Ads API calls
|
|
122
|
+
until a different identity is selected. The function also attempts
|
|
123
|
+
to pre-load credentials for the selected identity.
|
|
124
|
+
|
|
125
|
+
The function performs identity validation and credential pre-loading
|
|
126
|
+
to ensure the selected identity is ready for use. If credential
|
|
127
|
+
loading fails, the identity is still set but a warning is included
|
|
128
|
+
in the response.
|
|
129
|
+
|
|
130
|
+
:param request: Request containing the identity_id to activate
|
|
131
|
+
:type request: SetActiveIdentityRequest
|
|
132
|
+
:return: Response indicating success and the activated identity
|
|
133
|
+
:rtype: SetActiveIdentityResponse
|
|
134
|
+
:raises ValueError: If the specified identity is invalid
|
|
135
|
+
:raises Exception: If setting the active identity fails
|
|
136
|
+
|
|
137
|
+
.. example::
|
|
138
|
+
>>> request = SetActiveIdentityRequest(identity_id="abc123")
|
|
139
|
+
>>> response = await set_active_identity(request)
|
|
140
|
+
>>> if response.success:
|
|
141
|
+
... print("Identity activated successfully")
|
|
142
|
+
"""
|
|
143
|
+
logger.info(f"Setting active identity: {request.identity_id}")
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
auth_manager = get_auth_manager()
|
|
147
|
+
|
|
148
|
+
# Set the active identity
|
|
149
|
+
identity = await auth_manager.set_active_identity(request.identity_id)
|
|
150
|
+
|
|
151
|
+
# Try to pre-load credentials
|
|
152
|
+
credentials_loaded = False
|
|
153
|
+
message = None
|
|
154
|
+
try:
|
|
155
|
+
await auth_manager.get_active_credentials()
|
|
156
|
+
credentials_loaded = True
|
|
157
|
+
logger.info("Credentials pre-loaded successfully")
|
|
158
|
+
except Exception as e:
|
|
159
|
+
message = f"Identity set but credentials not loaded: {str(e)}"
|
|
160
|
+
logger.warning(message)
|
|
161
|
+
|
|
162
|
+
return SetActiveIdentityResponse(
|
|
163
|
+
success=True,
|
|
164
|
+
identity=identity,
|
|
165
|
+
credentials_loaded=credentials_loaded,
|
|
166
|
+
message=message,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
except ValueError as e:
|
|
170
|
+
logger.error(f"Invalid identity: {e}")
|
|
171
|
+
raise
|
|
172
|
+
except Exception as e:
|
|
173
|
+
logger.error(f"Failed to set active identity: {e}")
|
|
174
|
+
raise
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
async def get_identity_info(identity_id: str) -> Optional[Identity]:
|
|
178
|
+
"""Get detailed information about a specific remote identity.
|
|
179
|
+
|
|
180
|
+
Retrieves detailed information about a specific remote identity
|
|
181
|
+
by its unique identifier. This function is useful for getting
|
|
182
|
+
additional details about an identity before selecting it.
|
|
183
|
+
|
|
184
|
+
The function queries the authentication manager for the specified
|
|
185
|
+
identity and returns its details including attributes and metadata.
|
|
186
|
+
|
|
187
|
+
:param identity_id: The ID of the identity to retrieve
|
|
188
|
+
:type identity_id: str
|
|
189
|
+
:return: The Identity if found, None otherwise
|
|
190
|
+
:rtype: Optional[Identity]
|
|
191
|
+
:raises Exception: If retrieving identity info fails
|
|
192
|
+
|
|
193
|
+
.. example::
|
|
194
|
+
>>> identity = await get_identity_info("abc123")
|
|
195
|
+
>>> if identity:
|
|
196
|
+
... name = identity.attributes.get("name", "Unknown")
|
|
197
|
+
... print(f"Identity: {name}")
|
|
198
|
+
"""
|
|
199
|
+
logger.info(f"Getting identity info for: {identity_id}")
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
auth_manager = get_auth_manager()
|
|
203
|
+
identity = await auth_manager.get_identity(identity_id)
|
|
204
|
+
|
|
205
|
+
if identity:
|
|
206
|
+
name = identity.attributes.get("name", identity.id)
|
|
207
|
+
logger.info(f"Found identity: {name}")
|
|
208
|
+
else:
|
|
209
|
+
logger.info(f"Identity not found: {identity_id}")
|
|
210
|
+
|
|
211
|
+
return identity
|
|
212
|
+
except Exception as e:
|
|
213
|
+
logger.error(f"Failed to get identity info: {e}")
|
|
214
|
+
raise
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
async def list_identities() -> IdentityListResponse:
|
|
218
|
+
"""List all available identities.
|
|
219
|
+
|
|
220
|
+
Retrieves a list of all available identities from the current
|
|
221
|
+
authentication provider. This is a simplified wrapper around
|
|
222
|
+
list_remote_identities for common use cases.
|
|
223
|
+
|
|
224
|
+
This function uses the default identity type filter and is equivalent
|
|
225
|
+
to calling list_remote_identities() with no parameters.
|
|
226
|
+
|
|
227
|
+
:return: List of available identities
|
|
228
|
+
:rtype: IdentityListResponse
|
|
229
|
+
:raises Exception: If listing identities fails
|
|
230
|
+
|
|
231
|
+
.. example::
|
|
232
|
+
>>> identities = await list_identities()
|
|
233
|
+
>>> for identity in identities.identities:
|
|
234
|
+
... print(f"ID: {identity.id}")
|
|
235
|
+
"""
|
|
236
|
+
return await list_remote_identities()
|