awslabs.openapi-mcp-server 0.1.1__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 (38) hide show
  1. awslabs/__init__.py +16 -0
  2. awslabs/openapi_mcp_server/__init__.py +69 -0
  3. awslabs/openapi_mcp_server/api/__init__.py +18 -0
  4. awslabs/openapi_mcp_server/api/config.py +200 -0
  5. awslabs/openapi_mcp_server/auth/__init__.py +27 -0
  6. awslabs/openapi_mcp_server/auth/api_key_auth.py +185 -0
  7. awslabs/openapi_mcp_server/auth/auth_cache.py +190 -0
  8. awslabs/openapi_mcp_server/auth/auth_errors.py +206 -0
  9. awslabs/openapi_mcp_server/auth/auth_factory.py +146 -0
  10. awslabs/openapi_mcp_server/auth/auth_protocol.py +63 -0
  11. awslabs/openapi_mcp_server/auth/auth_provider.py +160 -0
  12. awslabs/openapi_mcp_server/auth/base_auth.py +218 -0
  13. awslabs/openapi_mcp_server/auth/basic_auth.py +171 -0
  14. awslabs/openapi_mcp_server/auth/bearer_auth.py +108 -0
  15. awslabs/openapi_mcp_server/auth/cognito_auth.py +538 -0
  16. awslabs/openapi_mcp_server/auth/register.py +100 -0
  17. awslabs/openapi_mcp_server/patch/__init__.py +17 -0
  18. awslabs/openapi_mcp_server/prompts/__init__.py +18 -0
  19. awslabs/openapi_mcp_server/prompts/generators/__init__.py +22 -0
  20. awslabs/openapi_mcp_server/prompts/generators/operation_prompts.py +642 -0
  21. awslabs/openapi_mcp_server/prompts/generators/workflow_prompts.py +257 -0
  22. awslabs/openapi_mcp_server/prompts/models.py +70 -0
  23. awslabs/openapi_mcp_server/prompts/prompt_manager.py +150 -0
  24. awslabs/openapi_mcp_server/server.py +511 -0
  25. awslabs/openapi_mcp_server/utils/__init__.py +18 -0
  26. awslabs/openapi_mcp_server/utils/cache_provider.py +249 -0
  27. awslabs/openapi_mcp_server/utils/config.py +35 -0
  28. awslabs/openapi_mcp_server/utils/error_handler.py +349 -0
  29. awslabs/openapi_mcp_server/utils/http_client.py +263 -0
  30. awslabs/openapi_mcp_server/utils/metrics_provider.py +503 -0
  31. awslabs/openapi_mcp_server/utils/openapi.py +217 -0
  32. awslabs/openapi_mcp_server/utils/openapi_validator.py +253 -0
  33. awslabs_openapi_mcp_server-0.1.1.dist-info/METADATA +418 -0
  34. awslabs_openapi_mcp_server-0.1.1.dist-info/RECORD +38 -0
  35. awslabs_openapi_mcp_server-0.1.1.dist-info/WHEEL +4 -0
  36. awslabs_openapi_mcp_server-0.1.1.dist-info/entry_points.txt +2 -0
  37. awslabs_openapi_mcp_server-0.1.1.dist-info/licenses/LICENSE +175 -0
  38. awslabs_openapi_mcp_server-0.1.1.dist-info/licenses/NOTICE +2 -0
@@ -0,0 +1,263 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """HTTP client utilities for the OpenAPI MCP Server.
15
+
16
+ This module provides enhanced HTTP client functionality with retry logic
17
+ and other improvements. It can use different backends based on configuration.
18
+ """
19
+
20
+ import asyncio
21
+ import httpx
22
+ from awslabs.openapi_mcp_server import logger
23
+ from awslabs.openapi_mcp_server.utils.config import (
24
+ HTTP_MAX_CONNECTIONS,
25
+ HTTP_MAX_KEEPALIVE,
26
+ USE_TENACITY,
27
+ )
28
+ from awslabs.openapi_mcp_server.utils.metrics_provider import api_call_timer
29
+ from typing import Any, Dict, Optional, Union
30
+
31
+
32
+ # Try to import tenacity if enabled
33
+ TENACITY_AVAILABLE = False
34
+ tenacity = None
35
+ if USE_TENACITY:
36
+ try:
37
+ import tenacity
38
+
39
+ TENACITY_AVAILABLE = True
40
+ logger.info('tenacity retry logic enabled')
41
+ except ImportError:
42
+ logger.warning('tenacity requested but not installed. Install with: pip install tenacity')
43
+
44
+
45
+ class HttpClientFactory:
46
+ """Factory for creating HTTP clients with enhanced functionality."""
47
+
48
+ @staticmethod
49
+ def create_client(
50
+ base_url: str,
51
+ headers: Optional[Dict[str, str]] = None,
52
+ auth: Optional[httpx.Auth] = None,
53
+ cookies: Optional[Dict[str, str]] = None,
54
+ timeout: Union[float, httpx.Timeout] = 30.0,
55
+ follow_redirects: bool = True,
56
+ max_connections: Optional[int] = None,
57
+ max_keepalive: Optional[int] = None,
58
+ ) -> httpx.AsyncClient:
59
+ """Create an HTTP client with enhanced functionality.
60
+
61
+ Args:
62
+ base_url: Base URL for the client
63
+ headers: Optional headers to include in requests
64
+ auth: Optional authentication to use
65
+ cookies: Optional cookies to include in requests
66
+ timeout: Request timeout in seconds
67
+ follow_redirects: Whether to follow redirects
68
+ max_connections: Maximum number of connections (defaults to config value)
69
+ max_keepalive: Maximum number of keepalive connections (defaults to config value)
70
+
71
+ Returns:
72
+ httpx.AsyncClient: The HTTP client
73
+
74
+ """
75
+ # Use configuration values if not explicitly provided
76
+ max_connections = max_connections if max_connections is not None else HTTP_MAX_CONNECTIONS
77
+ max_keepalive = max_keepalive if max_keepalive is not None else HTTP_MAX_KEEPALIVE
78
+
79
+ # Log detailed auth information
80
+ if auth:
81
+ auth_type = type(auth).__name__
82
+ has_session_manager = hasattr(auth, 'session_manager') if auth else False
83
+
84
+ logger.debug(f'Creating HTTP client with auth type: {auth_type}')
85
+
86
+ # For CognitoAuth, verify the session manager and token
87
+ if has_session_manager and hasattr(auth, 'session_manager'):
88
+ session_manager = getattr(auth, 'session_manager')
89
+ is_authenticated = (
90
+ session_manager.is_authenticated()
91
+ if hasattr(session_manager, 'is_authenticated')
92
+ else False
93
+ )
94
+ logger.debug(f'Auth has session_manager, authenticated: {is_authenticated}')
95
+
96
+ # Try to get and log the token
97
+ if hasattr(session_manager, 'get_access_token'):
98
+ token = session_manager.get_access_token()
99
+ has_token = token is not None
100
+ logger.debug(f'Session manager has access token: {has_token}')
101
+
102
+ if token:
103
+ # Mask token for security
104
+ masked_token = (
105
+ token[:10] + '...' + token[-10:] if len(token) > 30 else token
106
+ )
107
+ logger.debug(f'Access token from session manager: {masked_token}')
108
+
109
+ # Add token to default headers if not already there
110
+ if headers is None:
111
+ headers = {}
112
+
113
+ # Only add if not already in headers
114
+ if 'Authorization' not in headers:
115
+ headers['Authorization'] = f'Bearer {token}'
116
+ logger.debug('Added Authorization header from session token')
117
+
118
+ # Log the final headers that will be used (safely)
119
+ if headers:
120
+ safe_headers = {}
121
+ for key, value in headers.items():
122
+ if key.lower() == 'authorization' and value:
123
+ if isinstance(value, str) and value.startswith('Bearer '):
124
+ token_part = value[7:]
125
+ masked_token = (
126
+ token_part[:10] + '...' + token_part[-10:]
127
+ if len(token_part) > 30
128
+ else token_part
129
+ )
130
+ safe_headers[key] = f'Bearer {masked_token}'
131
+ else:
132
+ safe_headers[key] = '[MASKED]'
133
+ else:
134
+ safe_headers[key] = value
135
+ logger.debug(f'Creating client with headers: {safe_headers}')
136
+
137
+ # Create client with connection pooling
138
+ client = httpx.AsyncClient(
139
+ base_url=base_url,
140
+ headers=headers,
141
+ auth=auth,
142
+ cookies=cookies,
143
+ timeout=timeout if isinstance(timeout, httpx.Timeout) else httpx.Timeout(timeout),
144
+ follow_redirects=follow_redirects,
145
+ limits=httpx.Limits(
146
+ max_connections=max_connections,
147
+ max_keepalive_connections=max_keepalive,
148
+ ),
149
+ )
150
+
151
+ logger.info(
152
+ f'Created HTTP client for {base_url} with max_connections={max_connections}, '
153
+ f'max_keepalive={max_keepalive}'
154
+ )
155
+
156
+ # Verify client has auth after creation
157
+ client_has_auth = hasattr(client, 'auth') and client.auth is not None
158
+ logger.debug(f'Created client has auth: {client_has_auth}')
159
+ if client_has_auth:
160
+ logger.debug(f'Client auth type: {type(client.auth).__name__}')
161
+
162
+ return client
163
+
164
+
165
+ async def make_request_with_retry(
166
+ client: httpx.AsyncClient,
167
+ method: str,
168
+ url: str,
169
+ max_retries: int = 3,
170
+ retry_delay: float = 1.0,
171
+ **kwargs: Any,
172
+ ) -> httpx.Response:
173
+ """Make an HTTP request with retry logic.
174
+
175
+ Args:
176
+ client: The HTTP client
177
+ method: HTTP method
178
+ url: URL to request
179
+ max_retries: Maximum number of retries
180
+ retry_delay: Base delay between retries in seconds
181
+ **kwargs: Additional arguments to pass to the request
182
+
183
+ Returns:
184
+ httpx.Response: The HTTP response
185
+
186
+ Raises:
187
+ httpx.HTTPError: If the request fails after all retries
188
+
189
+ """
190
+ # Use tenacity if available and enabled
191
+ if USE_TENACITY and TENACITY_AVAILABLE and tenacity is not None:
192
+
193
+ @tenacity.retry(
194
+ stop=tenacity.stop_after_attempt(max_retries),
195
+ wait=tenacity.wait_exponential(
196
+ multiplier=retry_delay, min=retry_delay, max=retry_delay * 10
197
+ ),
198
+ retry=tenacity.retry_if_exception_type((httpx.TimeoutException, httpx.ConnectError)),
199
+ before_sleep=lambda retry_state: logger.warning(
200
+ f'Request failed, retrying ({retry_state.attempt_number}/{max_retries}): {retry_state.outcome.exception() if retry_state.outcome else "Unknown error"}'
201
+ ),
202
+ )
203
+ @api_call_timer
204
+ async def _make_request():
205
+ response = await client.request(method, url, **kwargs)
206
+ response.raise_for_status()
207
+ return response
208
+
209
+ return await _make_request()
210
+
211
+ # Otherwise, use simple retry logic
212
+ @api_call_timer
213
+ async def _make_request_simple():
214
+ for attempt in range(max_retries):
215
+ try:
216
+ response = await client.request(method, url, **kwargs)
217
+ response.raise_for_status()
218
+ return response
219
+ except (httpx.TimeoutException, httpx.ConnectError) as e:
220
+ if attempt < max_retries - 1:
221
+ delay = retry_delay * (2**attempt) # Exponential backoff
222
+ logger.warning(f'Request failed, retrying ({attempt + 1}/{max_retries}): {e}')
223
+ await asyncio.sleep(delay)
224
+ else:
225
+ logger.error(f'Request failed after {max_retries} attempts: {e}')
226
+ raise
227
+ except httpx.HTTPStatusError as e:
228
+ # Don't retry on status errors (4xx, 5xx)
229
+ logger.error(f'Request failed with status {e.response.status_code}: {e}')
230
+ raise
231
+
232
+ # This should never be reached
233
+ raise RuntimeError('Unexpected error in retry logic')
234
+
235
+ return await _make_request_simple()
236
+
237
+
238
+ # Simple function for making a single request without retries
239
+ @api_call_timer
240
+ async def make_request(
241
+ client: httpx.AsyncClient,
242
+ method: str,
243
+ url: str,
244
+ **kwargs: Any,
245
+ ) -> httpx.Response:
246
+ """Make an HTTP request without retry logic.
247
+
248
+ Args:
249
+ client: The HTTP client
250
+ method: HTTP method
251
+ url: URL to request
252
+ **kwargs: Additional arguments to pass to the request
253
+
254
+ Returns:
255
+ httpx.Response: The HTTP response
256
+
257
+ Raises:
258
+ httpx.HTTPError: If the request fails
259
+
260
+ """
261
+ response = await client.request(method, url, **kwargs)
262
+ response.raise_for_status()
263
+ return response