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,327 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Amazon Ads MCP Server - Modular Implementation.
|
|
3
|
+
|
|
4
|
+
This is a refactored version of the MCP server that uses modular components
|
|
5
|
+
for better maintainability and testability.
|
|
6
|
+
|
|
7
|
+
Implements the FastMCP server lifespan pattern for clean startup/shutdown.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import asyncio
|
|
12
|
+
import atexit
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
import signal
|
|
16
|
+
import sys
|
|
17
|
+
import types
|
|
18
|
+
from collections.abc import AsyncIterator
|
|
19
|
+
from contextlib import asynccontextmanager
|
|
20
|
+
from typing import Any, Optional
|
|
21
|
+
|
|
22
|
+
# Note: FASTMCP_EXPERIMENTAL_ENABLE_NEW_OPENAPI_PARSER removed in v0.1.18
|
|
23
|
+
# The new OpenAPI parser is now standard in FastMCP 2.14.0+
|
|
24
|
+
# See: https://gofastmcp.com/v2/development/upgrade-guide
|
|
25
|
+
|
|
26
|
+
from ..utils.http import http_client_manager
|
|
27
|
+
from ..utils.security import setup_secure_logging
|
|
28
|
+
from .server_builder import ServerBuilder
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@asynccontextmanager
|
|
34
|
+
async def server_lifespan(server: Any) -> AsyncIterator[None]:
|
|
35
|
+
"""Server lifespan context manager for clean startup and shutdown.
|
|
36
|
+
|
|
37
|
+
This lifespan function handles all server initialization and cleanup
|
|
38
|
+
in a single, testable async context manager. It's passed to the FastMCP
|
|
39
|
+
constructor to ensure proper resource management.
|
|
40
|
+
|
|
41
|
+
FastMCP 2.14+ passes the server instance to lifespan functions, which
|
|
42
|
+
enables access to server state during startup/shutdown.
|
|
43
|
+
|
|
44
|
+
Startup:
|
|
45
|
+
- Validates authentication configuration
|
|
46
|
+
- Initializes HTTP client connections
|
|
47
|
+
- Logs server state
|
|
48
|
+
|
|
49
|
+
Shutdown:
|
|
50
|
+
- Closes all HTTP client connections
|
|
51
|
+
- Shuts down the authentication manager
|
|
52
|
+
- Performs graceful cleanup
|
|
53
|
+
|
|
54
|
+
Usage:
|
|
55
|
+
server = FastMCP("Server", lifespan=server_lifespan)
|
|
56
|
+
|
|
57
|
+
:param server: The FastMCP server instance (provided by FastMCP)
|
|
58
|
+
:yield: Control to the running server
|
|
59
|
+
:rtype: AsyncIterator[None]
|
|
60
|
+
"""
|
|
61
|
+
_ = server # Server instance available if needed for future enhancements
|
|
62
|
+
logger.info("Server lifespan: Starting up...")
|
|
63
|
+
|
|
64
|
+
# Startup phase
|
|
65
|
+
try:
|
|
66
|
+
# Log initial state
|
|
67
|
+
from ..auth.manager import get_auth_manager
|
|
68
|
+
|
|
69
|
+
auth_mgr = get_auth_manager()
|
|
70
|
+
if auth_mgr and auth_mgr.provider:
|
|
71
|
+
provider_type = getattr(auth_mgr.provider, "provider_type", "unknown")
|
|
72
|
+
logger.info(f"Auth provider initialized: {provider_type}")
|
|
73
|
+
else:
|
|
74
|
+
logger.warning("No auth provider configured")
|
|
75
|
+
|
|
76
|
+
logger.info("Server lifespan: Startup complete")
|
|
77
|
+
except Exception as e:
|
|
78
|
+
logger.error(f"Server lifespan: Startup error: {e}")
|
|
79
|
+
raise
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
# Yield control to the running server
|
|
83
|
+
yield
|
|
84
|
+
finally:
|
|
85
|
+
# Shutdown phase - coordinate with fallback cleanup handlers
|
|
86
|
+
global _cleanup_done
|
|
87
|
+
|
|
88
|
+
if _cleanup_done:
|
|
89
|
+
logger.debug("Server lifespan: Cleanup already done by fallback handler")
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
logger.info("Server lifespan: Shutting down...")
|
|
93
|
+
|
|
94
|
+
# Close HTTP clients
|
|
95
|
+
try:
|
|
96
|
+
await http_client_manager.close_all()
|
|
97
|
+
logger.info("HTTP clients closed")
|
|
98
|
+
except Exception as e:
|
|
99
|
+
logger.error(f"Error closing HTTP clients: {e}")
|
|
100
|
+
|
|
101
|
+
# Close auth manager
|
|
102
|
+
try:
|
|
103
|
+
from ..auth.manager import get_auth_manager
|
|
104
|
+
|
|
105
|
+
am = get_auth_manager()
|
|
106
|
+
if am:
|
|
107
|
+
await am.close()
|
|
108
|
+
logger.info("Auth manager closed")
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.error(f"Error closing auth manager: {e}")
|
|
111
|
+
|
|
112
|
+
# Mark cleanup done to prevent fallback handlers from running again
|
|
113
|
+
_cleanup_done = True
|
|
114
|
+
logger.info("Server lifespan: Shutdown complete")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
async def create_amazon_ads_server() -> Any:
|
|
118
|
+
"""Create and configure the Amazon Ads MCP server using modular components.
|
|
119
|
+
|
|
120
|
+
This function creates a new Amazon Ads MCP server instance using the
|
|
121
|
+
modular ServerBuilder. The server is fully configured with builtin tools,
|
|
122
|
+
authentication middleware, and lifespan management.
|
|
123
|
+
|
|
124
|
+
The lifespan pattern (FastMCP 2.14+) handles:
|
|
125
|
+
- Clean startup with auth validation
|
|
126
|
+
- Graceful shutdown with resource cleanup
|
|
127
|
+
- HTTP client connection management
|
|
128
|
+
- Auth manager lifecycle
|
|
129
|
+
|
|
130
|
+
:return: Configured FastMCP server instance
|
|
131
|
+
:raises Exception: If server initialization fails
|
|
132
|
+
|
|
133
|
+
Examples
|
|
134
|
+
--------
|
|
135
|
+
.. code-block:: python
|
|
136
|
+
|
|
137
|
+
server = await create_amazon_ads_server()
|
|
138
|
+
await server.run()
|
|
139
|
+
"""
|
|
140
|
+
# Pass the lifespan to the builder for clean startup/shutdown
|
|
141
|
+
builder = ServerBuilder(lifespan=server_lifespan)
|
|
142
|
+
server = await builder.build()
|
|
143
|
+
|
|
144
|
+
# Built-in tools are registered in ServerBuilder._setup_builtin_tools()
|
|
145
|
+
# FastMCP handles prompts automatically
|
|
146
|
+
|
|
147
|
+
logger.info("MCP server setup complete (lifespan pattern enabled)")
|
|
148
|
+
return server
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
_cleanup_task = None
|
|
152
|
+
_cleanup_done = False
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
async def cleanup_resources_async() -> None:
|
|
156
|
+
"""Perform async cleanup of server resources.
|
|
157
|
+
|
|
158
|
+
This function performs asynchronous cleanup of server resources including
|
|
159
|
+
HTTP client connections and authentication manager shutdown. It ensures
|
|
160
|
+
that cleanup is only performed once and handles errors gracefully.
|
|
161
|
+
|
|
162
|
+
:raises Exception: If cleanup operations fail
|
|
163
|
+
"""
|
|
164
|
+
global _cleanup_done
|
|
165
|
+
if _cleanup_done:
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
logger.info("Shutting down server...")
|
|
169
|
+
try:
|
|
170
|
+
await http_client_manager.close_all()
|
|
171
|
+
logger.info("HTTP clients closed")
|
|
172
|
+
except Exception as e:
|
|
173
|
+
logger.error("Error closing http clients: %s", e)
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
from ..auth.manager import get_auth_manager
|
|
177
|
+
|
|
178
|
+
am = get_auth_manager()
|
|
179
|
+
if am:
|
|
180
|
+
await am.close()
|
|
181
|
+
logger.info("Auth manager closed")
|
|
182
|
+
except Exception as e:
|
|
183
|
+
logger.error("Error closing auth manager: %s", e)
|
|
184
|
+
|
|
185
|
+
_cleanup_done = True
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def cleanup_sync() -> None:
|
|
189
|
+
"""Synchronously clean up server resources.
|
|
190
|
+
|
|
191
|
+
This function performs cleanup operations for the server in a safe manner
|
|
192
|
+
that avoids creating new event loops in signal handlers.
|
|
193
|
+
|
|
194
|
+
The cleanup includes:
|
|
195
|
+
- Closing all HTTP client connections
|
|
196
|
+
- Shutting down the authentication manager
|
|
197
|
+
- Handling any cleanup errors gracefully
|
|
198
|
+
"""
|
|
199
|
+
global _cleanup_task, _cleanup_done
|
|
200
|
+
|
|
201
|
+
if _cleanup_done:
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
# Try to schedule cleanup in existing event loop
|
|
205
|
+
try:
|
|
206
|
+
loop = asyncio.get_running_loop()
|
|
207
|
+
if not loop.is_closed() and not _cleanup_task:
|
|
208
|
+
_cleanup_task = loop.create_task(cleanup_resources_async())
|
|
209
|
+
logger.debug("Cleanup scheduled in running event loop")
|
|
210
|
+
return
|
|
211
|
+
except RuntimeError:
|
|
212
|
+
# No running loop - that's OK for signal handlers
|
|
213
|
+
pass
|
|
214
|
+
|
|
215
|
+
# For atexit (not signal handlers), we can try more thorough cleanup
|
|
216
|
+
frame: Optional[types.FrameType] = sys._getframe()
|
|
217
|
+
# Check if we're in a signal handler by inspecting the stack
|
|
218
|
+
in_signal = False
|
|
219
|
+
while frame:
|
|
220
|
+
if frame.f_code.co_name in (
|
|
221
|
+
"<module>",
|
|
222
|
+
"<lambda>",
|
|
223
|
+
) and "signal" in str(frame.f_code.co_filename):
|
|
224
|
+
in_signal = True
|
|
225
|
+
break
|
|
226
|
+
frame = frame.f_back
|
|
227
|
+
|
|
228
|
+
if not in_signal and not _cleanup_done:
|
|
229
|
+
# Safe to create new event loop when not in signal handler
|
|
230
|
+
try:
|
|
231
|
+
loop = asyncio.new_event_loop()
|
|
232
|
+
asyncio.set_event_loop(loop)
|
|
233
|
+
loop.run_until_complete(cleanup_resources_async())
|
|
234
|
+
loop.close()
|
|
235
|
+
logger.info("Cleanup complete via new event loop")
|
|
236
|
+
except Exception as e:
|
|
237
|
+
logger.debug(f"Could not perform sync cleanup: {e}")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def main() -> None:
|
|
241
|
+
"""Run the Amazon Ads MCP server.
|
|
242
|
+
|
|
243
|
+
This is the main entry point for the Amazon Ads MCP server. It parses
|
|
244
|
+
command line arguments, initializes logging, creates the server, and
|
|
245
|
+
starts it with the specified transport.
|
|
246
|
+
|
|
247
|
+
The function supports multiple transport modes:
|
|
248
|
+
- stdio: Standard input/output communication
|
|
249
|
+
- http: HTTP-based communication
|
|
250
|
+
- streamable-http: Server-sent events HTTP communication
|
|
251
|
+
|
|
252
|
+
:raises KeyboardInterrupt: If the server is stopped by user interrupt
|
|
253
|
+
:raises Exception: If server initialization or startup fails
|
|
254
|
+
|
|
255
|
+
Examples
|
|
256
|
+
--------
|
|
257
|
+
.. code-block:: bash
|
|
258
|
+
|
|
259
|
+
# Run with HTTP transport
|
|
260
|
+
python -m amazon_ads_mcp.server.mcp_server --transport http --port 9080
|
|
261
|
+
|
|
262
|
+
# Run with stdio transport
|
|
263
|
+
python -m amazon_ads_mcp.server.mcp_server --transport stdio
|
|
264
|
+
"""
|
|
265
|
+
# Load environment variables from .env file if it exists
|
|
266
|
+
try:
|
|
267
|
+
from dotenv import load_dotenv
|
|
268
|
+
|
|
269
|
+
load_dotenv()
|
|
270
|
+
except ImportError:
|
|
271
|
+
pass
|
|
272
|
+
|
|
273
|
+
setup_secure_logging(level=os.environ.get("LOG_LEVEL", "INFO"))
|
|
274
|
+
logger.debug("Environment variables loaded")
|
|
275
|
+
|
|
276
|
+
parser = argparse.ArgumentParser(description="Amazon Ads MCP Server (Modular)")
|
|
277
|
+
parser.add_argument(
|
|
278
|
+
"--transport",
|
|
279
|
+
choices=["stdio", "http", "streamable-http"],
|
|
280
|
+
default="stdio",
|
|
281
|
+
)
|
|
282
|
+
parser.add_argument("--host", default="127.0.0.1")
|
|
283
|
+
parser.add_argument("--port", type=int, default=9080)
|
|
284
|
+
args = parser.parse_args()
|
|
285
|
+
|
|
286
|
+
# Set the port in environment for OAuth redirect URI
|
|
287
|
+
if args.transport in ("http", "streamable-http"):
|
|
288
|
+
os.environ["PORT"] = str(args.port)
|
|
289
|
+
|
|
290
|
+
# Register cleanup handlers
|
|
291
|
+
atexit.register(cleanup_sync)
|
|
292
|
+
signal.signal(signal.SIGTERM, lambda *_: cleanup_sync())
|
|
293
|
+
signal.signal(signal.SIGINT, lambda *_: cleanup_sync())
|
|
294
|
+
|
|
295
|
+
logger.info("Creating Amazon Ads MCP server...")
|
|
296
|
+
mcp = asyncio.run(create_amazon_ads_server())
|
|
297
|
+
|
|
298
|
+
# Small delay to ensure server is fully initialized
|
|
299
|
+
import time
|
|
300
|
+
|
|
301
|
+
time.sleep(0.5)
|
|
302
|
+
logger.info("Server initialization complete")
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
if args.transport in ("http", "streamable-http"):
|
|
306
|
+
transport = (
|
|
307
|
+
"streamable-http" if args.transport == "streamable-http" else "http"
|
|
308
|
+
)
|
|
309
|
+
logger.info("Starting %s server on %s:%d", transport, args.host, args.port)
|
|
310
|
+
# Use streamable-http transport which handles SSE properly
|
|
311
|
+
mcp.run(
|
|
312
|
+
transport=transport,
|
|
313
|
+
host=args.host,
|
|
314
|
+
port=args.port,
|
|
315
|
+
# Using default path to avoid redirect issues
|
|
316
|
+
)
|
|
317
|
+
else:
|
|
318
|
+
logger.info("Running in stdio mode")
|
|
319
|
+
mcp.run()
|
|
320
|
+
except KeyboardInterrupt:
|
|
321
|
+
logger.info("Server stopped by user")
|
|
322
|
+
finally:
|
|
323
|
+
cleanup_sync()
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
if __name__ == "__main__":
|
|
327
|
+
main()
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""OpenAPI utilities for the MCP server.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for processing OpenAPI specifications,
|
|
4
|
+
including slimming large descriptions and managing spec resources.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def truncate_text(text: Optional[str], max_len: int) -> Optional[str]:
|
|
11
|
+
"""Truncate text to a maximum length with ellipsis.
|
|
12
|
+
|
|
13
|
+
:param text: Text to truncate
|
|
14
|
+
:type text: Optional[str]
|
|
15
|
+
:param max_len: Maximum length
|
|
16
|
+
:type max_len: int
|
|
17
|
+
:return: Truncated text or original if shorter
|
|
18
|
+
:rtype: Optional[str]
|
|
19
|
+
"""
|
|
20
|
+
if not isinstance(text, str):
|
|
21
|
+
return text
|
|
22
|
+
if len(text) <= max_len:
|
|
23
|
+
return text
|
|
24
|
+
tail = "…"
|
|
25
|
+
return text[: max(0, max_len - len(tail))] + tail
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def slim_openapi_for_tools(spec: Dict[str, Any], max_desc: int = 200) -> None:
|
|
29
|
+
"""Reduce large descriptions in OpenAPI operations and parameters.
|
|
30
|
+
|
|
31
|
+
This helps keep tool metadata small when clients ingest tool definitions.
|
|
32
|
+
Modifies the spec in place.
|
|
33
|
+
|
|
34
|
+
:param spec: OpenAPI specification to slim
|
|
35
|
+
:type spec: Dict[str, Any]
|
|
36
|
+
:param max_desc: Maximum description length
|
|
37
|
+
:type max_desc: int
|
|
38
|
+
"""
|
|
39
|
+
try:
|
|
40
|
+
auth_header_names = {
|
|
41
|
+
"Authorization",
|
|
42
|
+
"Amazon-Advertising-API-ClientId",
|
|
43
|
+
"Amazon-Advertising-API-Scope",
|
|
44
|
+
}
|
|
45
|
+
auth_parameter_keys: set[str] = set()
|
|
46
|
+
|
|
47
|
+
def resolve_local_ref(ref: str) -> Any:
|
|
48
|
+
if not ref.startswith("#/"):
|
|
49
|
+
return None
|
|
50
|
+
current: Any = spec
|
|
51
|
+
for part in ref[2:].split("/"):
|
|
52
|
+
if not isinstance(current, dict) or part not in current:
|
|
53
|
+
return None
|
|
54
|
+
current = current[part]
|
|
55
|
+
return current
|
|
56
|
+
|
|
57
|
+
def is_auth_parameter_ref(ref: str) -> bool:
|
|
58
|
+
if not ref.startswith("#/components/parameters/"):
|
|
59
|
+
return False
|
|
60
|
+
key = ref.split("/")[-1]
|
|
61
|
+
return key in auth_parameter_keys
|
|
62
|
+
|
|
63
|
+
def is_auth_header_param(param: Dict[str, Any]) -> bool:
|
|
64
|
+
if (
|
|
65
|
+
param.get("in") == "header"
|
|
66
|
+
and param.get("name") in auth_header_names
|
|
67
|
+
):
|
|
68
|
+
return True
|
|
69
|
+
|
|
70
|
+
ref = param.get("$ref")
|
|
71
|
+
if isinstance(ref, str) and ref.startswith("#/"):
|
|
72
|
+
if is_auth_parameter_ref(ref):
|
|
73
|
+
return True
|
|
74
|
+
resolved = resolve_local_ref(ref)
|
|
75
|
+
if isinstance(resolved, dict):
|
|
76
|
+
return (
|
|
77
|
+
resolved.get("in") == "header"
|
|
78
|
+
and resolved.get("name") in auth_header_names
|
|
79
|
+
)
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
spec.pop("externalDocs", None)
|
|
83
|
+
|
|
84
|
+
# Fix server URLs that have descriptions in them
|
|
85
|
+
if "servers" in spec and isinstance(spec["servers"], list):
|
|
86
|
+
fixed_servers = []
|
|
87
|
+
for server in spec["servers"]:
|
|
88
|
+
if isinstance(server, dict) and "url" in server:
|
|
89
|
+
url = server["url"]
|
|
90
|
+
# Extract just the URL part if it contains description
|
|
91
|
+
if " (" in url:
|
|
92
|
+
url = url.split(" (")[0].strip()
|
|
93
|
+
fixed_servers.append({"url": url})
|
|
94
|
+
if fixed_servers:
|
|
95
|
+
# Use the first server as default (North America)
|
|
96
|
+
spec["servers"] = [fixed_servers[0]]
|
|
97
|
+
|
|
98
|
+
# Remove auth header params that are supplied by auth middleware/client
|
|
99
|
+
components = spec.get("components")
|
|
100
|
+
if isinstance(components, dict):
|
|
101
|
+
params = components.get("parameters")
|
|
102
|
+
if isinstance(params, dict):
|
|
103
|
+
for key, param in params.items():
|
|
104
|
+
if isinstance(param, dict) and is_auth_header_param(param):
|
|
105
|
+
auth_parameter_keys.add(key)
|
|
106
|
+
|
|
107
|
+
for p, methods in (spec.get("paths") or {}).items():
|
|
108
|
+
if not isinstance(methods, dict):
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
# Path-item parameters
|
|
112
|
+
path_params = methods.get("parameters") or []
|
|
113
|
+
if isinstance(path_params, list):
|
|
114
|
+
filtered_path_params = []
|
|
115
|
+
for prm in path_params:
|
|
116
|
+
if isinstance(prm, dict) and "description" in prm:
|
|
117
|
+
prm["description"] = truncate_text(prm.get("description"), max_desc)
|
|
118
|
+
if isinstance(prm, dict) and is_auth_header_param(prm):
|
|
119
|
+
continue
|
|
120
|
+
filtered_path_params.append(prm)
|
|
121
|
+
methods["parameters"] = filtered_path_params
|
|
122
|
+
|
|
123
|
+
for m, op in list(methods.items()):
|
|
124
|
+
if not isinstance(op, dict):
|
|
125
|
+
continue
|
|
126
|
+
# Trim top-level description
|
|
127
|
+
if "description" in op:
|
|
128
|
+
op["description"] = truncate_text(op.get("description"), max_desc)
|
|
129
|
+
# Prefer summary if description missing or too long
|
|
130
|
+
if not op.get("description") and op.get("summary"):
|
|
131
|
+
op["description"] = truncate_text(op.get("summary"), max_desc)
|
|
132
|
+
op.pop("externalDocs", None)
|
|
133
|
+
# Parameters
|
|
134
|
+
params = op.get("parameters") or []
|
|
135
|
+
if isinstance(params, list):
|
|
136
|
+
filtered_params = []
|
|
137
|
+
for prm in params:
|
|
138
|
+
if isinstance(prm, dict) and "description" in prm:
|
|
139
|
+
prm["description"] = truncate_text(
|
|
140
|
+
prm.get("description"), max_desc
|
|
141
|
+
)
|
|
142
|
+
if isinstance(prm, dict) and is_auth_header_param(prm):
|
|
143
|
+
continue
|
|
144
|
+
filtered_params.append(prm)
|
|
145
|
+
op["parameters"] = filtered_params
|
|
146
|
+
# Request body description
|
|
147
|
+
req = op.get("requestBody")
|
|
148
|
+
if isinstance(req, dict) and "description" in req:
|
|
149
|
+
req["description"] = truncate_text(req.get("description"), max_desc)
|
|
150
|
+
|
|
151
|
+
if auth_parameter_keys and isinstance(components, dict):
|
|
152
|
+
params = components.get("parameters")
|
|
153
|
+
if isinstance(params, dict):
|
|
154
|
+
for key in auth_parameter_keys:
|
|
155
|
+
params.pop(key, None)
|
|
156
|
+
except Exception:
|
|
157
|
+
# Do not fail mounting if slimming fails
|
|
158
|
+
pass
|