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.
- awslabs/__init__.py +16 -0
- awslabs/openapi_mcp_server/__init__.py +69 -0
- awslabs/openapi_mcp_server/api/__init__.py +18 -0
- awslabs/openapi_mcp_server/api/config.py +200 -0
- awslabs/openapi_mcp_server/auth/__init__.py +27 -0
- awslabs/openapi_mcp_server/auth/api_key_auth.py +185 -0
- awslabs/openapi_mcp_server/auth/auth_cache.py +190 -0
- awslabs/openapi_mcp_server/auth/auth_errors.py +206 -0
- awslabs/openapi_mcp_server/auth/auth_factory.py +146 -0
- awslabs/openapi_mcp_server/auth/auth_protocol.py +63 -0
- awslabs/openapi_mcp_server/auth/auth_provider.py +160 -0
- awslabs/openapi_mcp_server/auth/base_auth.py +218 -0
- awslabs/openapi_mcp_server/auth/basic_auth.py +171 -0
- awslabs/openapi_mcp_server/auth/bearer_auth.py +108 -0
- awslabs/openapi_mcp_server/auth/cognito_auth.py +538 -0
- awslabs/openapi_mcp_server/auth/register.py +100 -0
- awslabs/openapi_mcp_server/patch/__init__.py +17 -0
- awslabs/openapi_mcp_server/prompts/__init__.py +18 -0
- awslabs/openapi_mcp_server/prompts/generators/__init__.py +22 -0
- awslabs/openapi_mcp_server/prompts/generators/operation_prompts.py +642 -0
- awslabs/openapi_mcp_server/prompts/generators/workflow_prompts.py +257 -0
- awslabs/openapi_mcp_server/prompts/models.py +70 -0
- awslabs/openapi_mcp_server/prompts/prompt_manager.py +150 -0
- awslabs/openapi_mcp_server/server.py +511 -0
- awslabs/openapi_mcp_server/utils/__init__.py +18 -0
- awslabs/openapi_mcp_server/utils/cache_provider.py +249 -0
- awslabs/openapi_mcp_server/utils/config.py +35 -0
- awslabs/openapi_mcp_server/utils/error_handler.py +349 -0
- awslabs/openapi_mcp_server/utils/http_client.py +263 -0
- awslabs/openapi_mcp_server/utils/metrics_provider.py +503 -0
- awslabs/openapi_mcp_server/utils/openapi.py +217 -0
- awslabs/openapi_mcp_server/utils/openapi_validator.py +253 -0
- awslabs_openapi_mcp_server-0.1.1.dist-info/METADATA +418 -0
- awslabs_openapi_mcp_server-0.1.1.dist-info/RECORD +38 -0
- awslabs_openapi_mcp_server-0.1.1.dist-info/WHEEL +4 -0
- awslabs_openapi_mcp_server-0.1.1.dist-info/entry_points.txt +2 -0
- awslabs_openapi_mcp_server-0.1.1.dist-info/licenses/LICENSE +175 -0
- awslabs_openapi_mcp_server-0.1.1.dist-info/licenses/NOTICE +2 -0
awslabs/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
|
|
15
|
+
# This file is part of the awslabs namespace.
|
|
16
|
+
# It is intentionally minimal to support PEP 420 namespace packages.
|
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
"""
|
|
15
|
+
OpenAPI MCP Server - A server that dynamically creates MCP tools and resources from OpenAPI specifications.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
__version__ = '0.1.0'
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
import inspect
|
|
22
|
+
import sys
|
|
23
|
+
|
|
24
|
+
from loguru import logger
|
|
25
|
+
|
|
26
|
+
# Remove default loguru handler
|
|
27
|
+
logger.remove()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_format():
|
|
31
|
+
return '<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>'
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Set up enhanced logging format to include function name, line number, and logger name
|
|
35
|
+
# Fixed the whitespace issue after log level by removing padding
|
|
36
|
+
logger.add(
|
|
37
|
+
sys.stdout,
|
|
38
|
+
format=get_format(),
|
|
39
|
+
level='INFO',
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_caller_info():
|
|
44
|
+
"""Get information about the caller of a function.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
str: A string containing information about the caller
|
|
48
|
+
"""
|
|
49
|
+
# Get the current frame
|
|
50
|
+
current_frame = inspect.currentframe()
|
|
51
|
+
if not current_frame:
|
|
52
|
+
return 'unknown'
|
|
53
|
+
|
|
54
|
+
# Go up one frame
|
|
55
|
+
parent_frame = current_frame.f_back
|
|
56
|
+
if not parent_frame:
|
|
57
|
+
return 'unknown'
|
|
58
|
+
|
|
59
|
+
# Go up another frame to find the caller
|
|
60
|
+
caller_frame = parent_frame.f_back
|
|
61
|
+
if not caller_frame:
|
|
62
|
+
return 'unknown'
|
|
63
|
+
|
|
64
|
+
# Get filename, function name, and line number
|
|
65
|
+
caller_info = inspect.getframeinfo(caller_frame)
|
|
66
|
+
return f'{caller_info.filename}:{caller_info.function}:{caller_info.lineno}'
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
__all__ = ['__version__', 'logger', 'get_caller_info']
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
"""API handling modules for OpenAPI MCP Server."""
|
|
15
|
+
|
|
16
|
+
from awslabs.openapi_mcp_server.api.config import Config, load_config
|
|
17
|
+
|
|
18
|
+
__all__ = ['Config', 'load_config']
|
|
@@ -0,0 +1,200 @@
|
|
|
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
|
+
"""Configuration module for the OpenAPI MCP Server."""
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
from awslabs.openapi_mcp_server import get_caller_info, logger
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class Config:
|
|
24
|
+
"""Configuration for the OpenAPI MCP Server."""
|
|
25
|
+
|
|
26
|
+
# API information
|
|
27
|
+
api_name: str = 'awslabs-openapi-mcp-server'
|
|
28
|
+
api_base_url: str = 'https://localhost:8000'
|
|
29
|
+
api_spec_url: str = ''
|
|
30
|
+
api_spec_path: str = ''
|
|
31
|
+
|
|
32
|
+
# Authentication
|
|
33
|
+
auth_type: str = 'none' # none, basic, bearer, api_key, cognito
|
|
34
|
+
auth_username: str = ''
|
|
35
|
+
auth_password: str = ''
|
|
36
|
+
auth_token: str = ''
|
|
37
|
+
auth_api_key: str = ''
|
|
38
|
+
auth_api_key_name: str = 'api_key'
|
|
39
|
+
auth_api_key_in: str = 'header' # header, query, cookie
|
|
40
|
+
|
|
41
|
+
# Cognito authentication
|
|
42
|
+
auth_cognito_client_id: str = ''
|
|
43
|
+
auth_cognito_username: str = ''
|
|
44
|
+
auth_cognito_password: str = ''
|
|
45
|
+
auth_cognito_user_pool_id: str = ''
|
|
46
|
+
auth_cognito_region: str = 'us-east-1'
|
|
47
|
+
|
|
48
|
+
# Server configuration
|
|
49
|
+
port: int = 8000
|
|
50
|
+
# Default to localhost for security; use SERVER_HOST env var to override when needed (e.g. in Docker)
|
|
51
|
+
host: str = '127.0.0.1'
|
|
52
|
+
debug: bool = False
|
|
53
|
+
transport: str = 'stdio' # stdio only
|
|
54
|
+
message_timeout: int = 60
|
|
55
|
+
version: str = '0.1.0'
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def load_config(args: Any = None) -> Config:
|
|
59
|
+
"""Load configuration from arguments and environment variables.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
args: Command line arguments
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Config: Configuration object
|
|
66
|
+
|
|
67
|
+
"""
|
|
68
|
+
logger.debug('Loading configuration')
|
|
69
|
+
|
|
70
|
+
# Get caller information for debugging
|
|
71
|
+
caller_info = get_caller_info()
|
|
72
|
+
logger.debug(f'Called from {caller_info}')
|
|
73
|
+
|
|
74
|
+
# Create default config
|
|
75
|
+
config = Config()
|
|
76
|
+
|
|
77
|
+
# Load from environment variables
|
|
78
|
+
env_vars = {
|
|
79
|
+
# API information
|
|
80
|
+
'API_NAME': (lambda v: setattr(config, 'api_name', v)),
|
|
81
|
+
'API_BASE_URL': (lambda v: setattr(config, 'api_base_url', v)),
|
|
82
|
+
'API_SPEC_URL': (lambda v: setattr(config, 'api_spec_url', v)),
|
|
83
|
+
'API_SPEC_PATH': (lambda v: setattr(config, 'api_spec_path', v)),
|
|
84
|
+
# Authentication
|
|
85
|
+
'AUTH_TYPE': (lambda v: setattr(config, 'auth_type', v)),
|
|
86
|
+
'AUTH_USERNAME': (lambda v: setattr(config, 'auth_username', v)),
|
|
87
|
+
'AUTH_PASSWORD': (lambda v: setattr(config, 'auth_password', v)),
|
|
88
|
+
'AUTH_TOKEN': (lambda v: setattr(config, 'auth_token', v)),
|
|
89
|
+
'AUTH_API_KEY': (lambda v: setattr(config, 'auth_api_key', v)),
|
|
90
|
+
'AUTH_API_KEY_NAME': (lambda v: setattr(config, 'auth_api_key_name', v)),
|
|
91
|
+
'AUTH_API_KEY_IN': (lambda v: setattr(config, 'auth_api_key_in', v)),
|
|
92
|
+
# Cognito authentication environment variables
|
|
93
|
+
'AUTH_COGNITO_CLIENT_ID': (lambda v: setattr(config, 'auth_cognito_client_id', v)),
|
|
94
|
+
'AUTH_COGNITO_USERNAME': (lambda v: setattr(config, 'auth_cognito_username', v)),
|
|
95
|
+
'AUTH_COGNITO_PASSWORD': (lambda v: setattr(config, 'auth_cognito_password', v)),
|
|
96
|
+
'AUTH_COGNITO_USER_POOL_ID': (lambda v: setattr(config, 'auth_cognito_user_pool_id', v)),
|
|
97
|
+
'AUTH_COGNITO_REGION': (lambda v: setattr(config, 'auth_cognito_region', v)),
|
|
98
|
+
# Server configuration
|
|
99
|
+
'SERVER_PORT': (lambda v: setattr(config, 'port', int(v))),
|
|
100
|
+
'SERVER_HOST': (lambda v: setattr(config, 'host', v)),
|
|
101
|
+
'SERVER_DEBUG': (lambda v: setattr(config, 'debug', v.lower() == 'true')),
|
|
102
|
+
'SERVER_TRANSPORT': (lambda v: setattr(config, 'transport', v)),
|
|
103
|
+
'SERVER_MESSAGE_TIMEOUT': (lambda v: setattr(config, 'message_timeout', int(v))),
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# Load environment variables
|
|
107
|
+
env_loaded = {}
|
|
108
|
+
for key, setter in env_vars.items():
|
|
109
|
+
if key in os.environ:
|
|
110
|
+
env_value = os.environ[key]
|
|
111
|
+
setter(env_value)
|
|
112
|
+
env_loaded[key] = env_value
|
|
113
|
+
|
|
114
|
+
if env_loaded:
|
|
115
|
+
logger.debug(
|
|
116
|
+
f'Loaded {len(env_loaded)} environment variables: {", ".join(env_loaded.keys())}'
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Load from arguments
|
|
120
|
+
if args:
|
|
121
|
+
if hasattr(args, 'api_name') and args.api_name:
|
|
122
|
+
logger.debug(f'Setting API name from arguments: {args.api_name}')
|
|
123
|
+
config.api_name = args.api_name
|
|
124
|
+
|
|
125
|
+
if hasattr(args, 'api_url') and args.api_url:
|
|
126
|
+
logger.debug(f'Setting API base URL from arguments: {args.api_url}')
|
|
127
|
+
config.api_base_url = args.api_url
|
|
128
|
+
|
|
129
|
+
if hasattr(args, 'spec_url') and args.spec_url:
|
|
130
|
+
logger.debug(f'Setting API spec URL from arguments: {args.spec_url}')
|
|
131
|
+
config.api_spec_url = args.spec_url
|
|
132
|
+
|
|
133
|
+
if hasattr(args, 'spec_path') and args.spec_path:
|
|
134
|
+
logger.debug(f'Setting API spec path from arguments: {args.spec_path}')
|
|
135
|
+
config.api_spec_path = args.spec_path
|
|
136
|
+
|
|
137
|
+
if hasattr(args, 'port') and args.port:
|
|
138
|
+
logger.debug(f'Setting port from arguments: {args.port}')
|
|
139
|
+
config.port = args.port
|
|
140
|
+
|
|
141
|
+
if hasattr(args, 'debug') and args.debug:
|
|
142
|
+
logger.debug('Setting debug mode from arguments')
|
|
143
|
+
config.debug = True
|
|
144
|
+
|
|
145
|
+
# Authentication arguments
|
|
146
|
+
if hasattr(args, 'auth_type') and args.auth_type:
|
|
147
|
+
logger.debug(f'Setting auth type from arguments: {args.auth_type}')
|
|
148
|
+
config.auth_type = args.auth_type
|
|
149
|
+
|
|
150
|
+
if hasattr(args, 'auth_username') and args.auth_username:
|
|
151
|
+
logger.debug('Setting auth username from arguments')
|
|
152
|
+
config.auth_username = args.auth_username
|
|
153
|
+
|
|
154
|
+
if hasattr(args, 'auth_password') and args.auth_password:
|
|
155
|
+
logger.debug('Setting auth password from arguments')
|
|
156
|
+
config.auth_password = args.auth_password
|
|
157
|
+
|
|
158
|
+
if hasattr(args, 'auth_token') and args.auth_token:
|
|
159
|
+
logger.debug('Setting auth token from arguments')
|
|
160
|
+
config.auth_token = args.auth_token
|
|
161
|
+
|
|
162
|
+
if hasattr(args, 'auth_api_key') and args.auth_api_key:
|
|
163
|
+
logger.debug('Setting auth API key from arguments')
|
|
164
|
+
config.auth_api_key = args.auth_api_key
|
|
165
|
+
|
|
166
|
+
if hasattr(args, 'auth_api_key_name') and args.auth_api_key_name:
|
|
167
|
+
logger.debug(f'Setting auth API key name from arguments: {args.auth_api_key_name}')
|
|
168
|
+
config.auth_api_key_name = args.auth_api_key_name
|
|
169
|
+
|
|
170
|
+
if hasattr(args, 'auth_api_key_in') and args.auth_api_key_in:
|
|
171
|
+
logger.debug(f'Setting auth API key location from arguments: {args.auth_api_key_in}')
|
|
172
|
+
config.auth_api_key_in = args.auth_api_key_in
|
|
173
|
+
|
|
174
|
+
# Cognito authentication arguments
|
|
175
|
+
if hasattr(args, 'auth_cognito_client_id') and args.auth_cognito_client_id:
|
|
176
|
+
logger.debug('Setting Cognito client ID from arguments')
|
|
177
|
+
config.auth_cognito_client_id = args.auth_cognito_client_id
|
|
178
|
+
|
|
179
|
+
if hasattr(args, 'auth_cognito_username') and args.auth_cognito_username:
|
|
180
|
+
logger.debug('Setting Cognito username from arguments')
|
|
181
|
+
config.auth_cognito_username = args.auth_cognito_username
|
|
182
|
+
|
|
183
|
+
if hasattr(args, 'auth_cognito_password') and args.auth_cognito_password:
|
|
184
|
+
logger.debug('Setting Cognito password from arguments')
|
|
185
|
+
config.auth_cognito_password = args.auth_cognito_password
|
|
186
|
+
|
|
187
|
+
if hasattr(args, 'auth_cognito_user_pool_id') and args.auth_cognito_user_pool_id:
|
|
188
|
+
logger.debug('Setting Cognito user pool ID from arguments')
|
|
189
|
+
config.auth_cognito_user_pool_id = args.auth_cognito_user_pool_id
|
|
190
|
+
|
|
191
|
+
if hasattr(args, 'auth_cognito_region') and args.auth_cognito_region:
|
|
192
|
+
logger.debug(f'Setting Cognito region from arguments: {args.auth_cognito_region}')
|
|
193
|
+
config.auth_cognito_region = args.auth_cognito_region
|
|
194
|
+
|
|
195
|
+
# Log final configuration details
|
|
196
|
+
logger.info(
|
|
197
|
+
f'Configuration loaded: API name={config.api_name}, transport={config.transport}, port={config.port}'
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
return config
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
"""Authentication package for OpenAPI MCP Server."""
|
|
15
|
+
|
|
16
|
+
# Import register module to auto-register providers
|
|
17
|
+
import awslabs.openapi_mcp_server.auth.register # noqa: F401
|
|
18
|
+
from awslabs.openapi_mcp_server.auth.auth_factory import get_auth_provider, is_auth_type_available
|
|
19
|
+
from awslabs.openapi_mcp_server.auth.auth_provider import AuthProvider, NullAuthProvider
|
|
20
|
+
|
|
21
|
+
# Define public exports
|
|
22
|
+
__all__ = [
|
|
23
|
+
'get_auth_provider',
|
|
24
|
+
'is_auth_type_available',
|
|
25
|
+
'AuthProvider',
|
|
26
|
+
'NullAuthProvider',
|
|
27
|
+
]
|
|
@@ -0,0 +1,185 @@
|
|
|
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
|
+
"""API Key authentication provider."""
|
|
15
|
+
|
|
16
|
+
import bcrypt
|
|
17
|
+
from awslabs.openapi_mcp_server import logger
|
|
18
|
+
from awslabs.openapi_mcp_server.api.config import Config
|
|
19
|
+
from awslabs.openapi_mcp_server.auth.auth_cache import cached_auth_data
|
|
20
|
+
from awslabs.openapi_mcp_server.auth.auth_errors import (
|
|
21
|
+
ConfigurationError,
|
|
22
|
+
MissingCredentialsError,
|
|
23
|
+
)
|
|
24
|
+
from awslabs.openapi_mcp_server.auth.base_auth import BaseAuthProvider
|
|
25
|
+
from typing import Dict
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ApiKeyAuthProvider(BaseAuthProvider):
|
|
29
|
+
"""API Key authentication provider.
|
|
30
|
+
|
|
31
|
+
This provider adds an API key to requests, either in a header,
|
|
32
|
+
query parameter, or cookie.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, config: Config):
|
|
36
|
+
"""Initialize with configuration.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
config: Application configuration
|
|
40
|
+
|
|
41
|
+
"""
|
|
42
|
+
# Store configuration values before calling super().__init__
|
|
43
|
+
# so they're available during validation
|
|
44
|
+
self._api_key = config.auth_api_key
|
|
45
|
+
self._api_key_name = config.auth_api_key_name or 'api_key'
|
|
46
|
+
self._api_key_in = config.auth_api_key_in or 'header'
|
|
47
|
+
self._api_key_hash = None
|
|
48
|
+
|
|
49
|
+
# Call parent initializer which will validate and initialize auth
|
|
50
|
+
super().__init__(config)
|
|
51
|
+
|
|
52
|
+
def _validate_config(self) -> bool:
|
|
53
|
+
"""Validate the configuration.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
bool: True if API key is provided, False otherwise
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
MissingCredentialsError: If API key is missing
|
|
60
|
+
ConfigurationError: If API key location is invalid
|
|
61
|
+
|
|
62
|
+
"""
|
|
63
|
+
if not self._api_key:
|
|
64
|
+
raise MissingCredentialsError(
|
|
65
|
+
'API Key authentication requires a valid API key',
|
|
66
|
+
{
|
|
67
|
+
'help': 'Provide it using --auth-api-key command line argument or AUTH_API_KEY environment variable'
|
|
68
|
+
},
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if self._api_key_in not in ('header', 'query', 'cookie'):
|
|
72
|
+
raise ConfigurationError(
|
|
73
|
+
f'Invalid API key location: {self._api_key_in}',
|
|
74
|
+
{
|
|
75
|
+
'valid_locations': ['header', 'query', 'cookie'],
|
|
76
|
+
'help': 'Provide a valid location using --auth-api-key-in command line argument or AUTH_API_KEY_IN environment variable',
|
|
77
|
+
},
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Create a hash of the API key for caching
|
|
81
|
+
self._api_key_hash = self._hash_api_key(self._api_key)
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
def _handle_validation_error(self) -> None:
|
|
85
|
+
"""Handle validation error."""
|
|
86
|
+
# This should not be called since we raise exceptions in _validate_config
|
|
87
|
+
# But we implement it for completeness
|
|
88
|
+
self._validation_error = MissingCredentialsError(
|
|
89
|
+
'API Key authentication requires a valid API key',
|
|
90
|
+
{
|
|
91
|
+
'help': 'Provide it using --auth-api-key command line argument or AUTH_API_KEY environment variable'
|
|
92
|
+
},
|
|
93
|
+
)
|
|
94
|
+
self._log_auth_error(self._validation_error)
|
|
95
|
+
|
|
96
|
+
def _initialize_auth(self) -> None:
|
|
97
|
+
"""Initialize authentication data after validation."""
|
|
98
|
+
# Use cached methods to generate auth data based on location
|
|
99
|
+
if self._api_key_in == 'header':
|
|
100
|
+
self._auth_headers = self._generate_auth_headers(self._api_key_hash, self._api_key_name)
|
|
101
|
+
elif self._api_key_in == 'query':
|
|
102
|
+
self._auth_params = self._generate_auth_params(self._api_key_hash, self._api_key_name)
|
|
103
|
+
elif self._api_key_in == 'cookie':
|
|
104
|
+
self._auth_cookies = self._generate_auth_cookies(self._api_key_hash, self._api_key_name)
|
|
105
|
+
|
|
106
|
+
@staticmethod
|
|
107
|
+
def _hash_api_key(api_key: str) -> str:
|
|
108
|
+
"""Create a hash of the API key for caching.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
api_key: API key
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
str: Hash of the API key
|
|
115
|
+
|
|
116
|
+
"""
|
|
117
|
+
# Create a hash of the API key to use as a cache key
|
|
118
|
+
return bcrypt.hashpw(api_key.encode('utf-8'), bcrypt.gensalt(rounds=10)).hex()
|
|
119
|
+
|
|
120
|
+
@cached_auth_data(ttl=3600) # Cache for 1 hour by default
|
|
121
|
+
def _generate_auth_headers(self, api_key_hash: str, api_key_name: str) -> Dict[str, str]:
|
|
122
|
+
"""Generate authentication headers.
|
|
123
|
+
|
|
124
|
+
This method is cached to avoid regenerating headers for the same API key.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
api_key_hash: Hash of the API key
|
|
128
|
+
api_key_name: Name of the API key header
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Dict[str, str]: Authentication headers
|
|
132
|
+
|
|
133
|
+
"""
|
|
134
|
+
logger.debug(f'Generating new API key headers with name: {api_key_name}')
|
|
135
|
+
# Log key length for debugging without exposing the key
|
|
136
|
+
logger.debug(f'API key length: {len(self._api_key) if self._api_key else 0} characters')
|
|
137
|
+
return {api_key_name: self._api_key}
|
|
138
|
+
|
|
139
|
+
@cached_auth_data(ttl=3600) # Cache for 1 hour by default
|
|
140
|
+
def _generate_auth_params(self, api_key_hash: str, api_key_name: str) -> Dict[str, str]:
|
|
141
|
+
"""Generate authentication query parameters.
|
|
142
|
+
|
|
143
|
+
This method is cached to avoid regenerating parameters for the same API key.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
api_key_hash: Hash of the API key
|
|
147
|
+
api_key_name: Name of the API key parameter
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Dict[str, str]: Authentication query parameters
|
|
151
|
+
|
|
152
|
+
"""
|
|
153
|
+
logger.debug(f'Generating new API key query parameters with name: {api_key_name}')
|
|
154
|
+
# Log key length for debugging without exposing the key
|
|
155
|
+
logger.debug(f'API key length: {len(self._api_key) if self._api_key else 0} characters')
|
|
156
|
+
return {api_key_name: self._api_key}
|
|
157
|
+
|
|
158
|
+
@cached_auth_data(ttl=3600) # Cache for 1 hour by default
|
|
159
|
+
def _generate_auth_cookies(self, api_key_hash: str, api_key_name: str) -> Dict[str, str]:
|
|
160
|
+
"""Generate authentication cookies.
|
|
161
|
+
|
|
162
|
+
This method is cached to avoid regenerating cookies for the same API key.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
api_key_hash: Hash of the API key
|
|
166
|
+
api_key_name: Name of the API key cookie
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Dict[str, str]: Authentication cookies
|
|
170
|
+
|
|
171
|
+
"""
|
|
172
|
+
logger.debug(f'Generating new API key cookies with name: {api_key_name}')
|
|
173
|
+
# Log key length for debugging without exposing the key
|
|
174
|
+
logger.debug(f'API key length: {len(self._api_key) if self._api_key else 0} characters')
|
|
175
|
+
return {api_key_name: self._api_key}
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def provider_name(self) -> str:
|
|
179
|
+
"""Get the name of the authentication provider.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
str: Name of the authentication provider
|
|
183
|
+
|
|
184
|
+
"""
|
|
185
|
+
return 'api_key'
|