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
|
@@ -0,0 +1,511 @@
|
|
|
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
|
+
"""awslabs openapi MCP Server implementation."""
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import asyncio
|
|
18
|
+
import httpx
|
|
19
|
+
import re
|
|
20
|
+
import signal
|
|
21
|
+
import sys
|
|
22
|
+
|
|
23
|
+
# Import from our modules - use direct imports from sub-modules for better patching in tests
|
|
24
|
+
from awslabs.openapi_mcp_server import logger
|
|
25
|
+
from awslabs.openapi_mcp_server.api.config import Config, load_config
|
|
26
|
+
from awslabs.openapi_mcp_server.prompts import MCPPromptManager
|
|
27
|
+
from awslabs.openapi_mcp_server.utils.http_client import HttpClientFactory, make_request_with_retry
|
|
28
|
+
from awslabs.openapi_mcp_server.utils.metrics_provider import metrics
|
|
29
|
+
from awslabs.openapi_mcp_server.utils.openapi import load_openapi_spec
|
|
30
|
+
from awslabs.openapi_mcp_server.utils.openapi_validator import validate_openapi_spec
|
|
31
|
+
from fastmcp import FastMCP
|
|
32
|
+
from fastmcp.server.openapi import FastMCPOpenAPI, RouteMap, RouteType
|
|
33
|
+
from typing import Any, Dict
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def create_mcp_server(config: Config) -> FastMCP:
|
|
37
|
+
"""Create and configure the FastMCP server.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
config: Server configuration
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
FastMCP: The configured FastMCP server
|
|
44
|
+
|
|
45
|
+
"""
|
|
46
|
+
# Log environment information
|
|
47
|
+
logger.debug('Environment information:')
|
|
48
|
+
logger.debug(f'Python version: {sys.version}')
|
|
49
|
+
try:
|
|
50
|
+
logger.debug(f'HTTPX version: {httpx.__version__}')
|
|
51
|
+
except AttributeError:
|
|
52
|
+
logger.debug('HTTPX version: unknown')
|
|
53
|
+
|
|
54
|
+
logger.info('Creating FastMCP server')
|
|
55
|
+
|
|
56
|
+
# Create the FastMCP server
|
|
57
|
+
server = FastMCP(
|
|
58
|
+
'awslabs.openapi-mcp-server',
|
|
59
|
+
instructions='This server acts as a bridge between OpenAPI specifications and LLMs, allowing models to have a better understanding of available API capabilities without requiring manual tool definitions.',
|
|
60
|
+
dependencies=[
|
|
61
|
+
'pydantic',
|
|
62
|
+
'loguru',
|
|
63
|
+
'httpx',
|
|
64
|
+
],
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
# Load OpenAPI spec
|
|
69
|
+
if not config.api_spec_url and not config.api_spec_path:
|
|
70
|
+
logger.error('No API spec URL or path provided')
|
|
71
|
+
raise ValueError('Either api_spec_url or api_spec_path must be provided')
|
|
72
|
+
|
|
73
|
+
logger.debug(
|
|
74
|
+
f'Loading OpenAPI spec from URL: {config.api_spec_url} or path: {config.api_spec_path}'
|
|
75
|
+
)
|
|
76
|
+
openapi_spec = load_openapi_spec(url=config.api_spec_url, path=config.api_spec_path)
|
|
77
|
+
|
|
78
|
+
# Validate the OpenAPI spec
|
|
79
|
+
if not validate_openapi_spec(openapi_spec):
|
|
80
|
+
logger.warning('OpenAPI specification validation failed, but continuing anyway')
|
|
81
|
+
|
|
82
|
+
# Create a client for the API
|
|
83
|
+
if not config.api_base_url:
|
|
84
|
+
logger.error('No API base URL provided')
|
|
85
|
+
raise ValueError('API base URL must be provided')
|
|
86
|
+
|
|
87
|
+
# Configure authentication using the auth factory
|
|
88
|
+
from awslabs.openapi_mcp_server.auth import get_auth_provider, is_auth_type_available
|
|
89
|
+
|
|
90
|
+
# Import and register the specific auth provider
|
|
91
|
+
from awslabs.openapi_mcp_server.auth.register import register_provider_by_type
|
|
92
|
+
|
|
93
|
+
# Register only the provider we need
|
|
94
|
+
if config.auth_type and config.auth_type != 'none':
|
|
95
|
+
logger.debug(f'Registering authentication provider for type: {config.auth_type}')
|
|
96
|
+
register_provider_by_type(config.auth_type)
|
|
97
|
+
else:
|
|
98
|
+
logger.debug('No authentication type specified, using none')
|
|
99
|
+
|
|
100
|
+
# Check if the requested auth type is available
|
|
101
|
+
if config.auth_type != 'none' and not is_auth_type_available(config.auth_type):
|
|
102
|
+
logger.warning(
|
|
103
|
+
f'Authentication type {config.auth_type} is not available. Falling back to none.'
|
|
104
|
+
)
|
|
105
|
+
config.auth_type = 'none'
|
|
106
|
+
|
|
107
|
+
# Get the auth provider
|
|
108
|
+
auth_provider = get_auth_provider(config)
|
|
109
|
+
|
|
110
|
+
# Get authentication components
|
|
111
|
+
auth_headers = auth_provider.get_auth_headers()
|
|
112
|
+
# Get auth params (not used directly but may be needed in the future)
|
|
113
|
+
_ = auth_provider.get_auth_params()
|
|
114
|
+
auth_cookies = auth_provider.get_auth_cookies()
|
|
115
|
+
httpx_auth = auth_provider.get_httpx_auth()
|
|
116
|
+
|
|
117
|
+
# Helper function to handle authentication configuration errors
|
|
118
|
+
def handle_auth_error(auth_type, error_message):
|
|
119
|
+
"""Handle authentication configuration errors.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
auth_type: The authentication type
|
|
123
|
+
error_message: The error message to log
|
|
124
|
+
|
|
125
|
+
"""
|
|
126
|
+
logger.error(
|
|
127
|
+
f'Authentication provider {auth_provider.provider_name} is not properly configured'
|
|
128
|
+
)
|
|
129
|
+
logger.error(error_message)
|
|
130
|
+
logger.error('Server shutting down due to authentication configuration error.')
|
|
131
|
+
sys.exit(1)
|
|
132
|
+
|
|
133
|
+
# Check if the provider is properly configured
|
|
134
|
+
if not auth_provider.is_configured() and config.auth_type != 'none':
|
|
135
|
+
if config.auth_type == 'bearer':
|
|
136
|
+
handle_auth_error(
|
|
137
|
+
'bearer',
|
|
138
|
+
'Bearer authentication requires a valid token. Please provide a token using --auth-token command line argument or AUTH_TOKEN environment variable.',
|
|
139
|
+
)
|
|
140
|
+
elif config.auth_type == 'basic':
|
|
141
|
+
handle_auth_error(
|
|
142
|
+
'basic',
|
|
143
|
+
'Basic authentication requires both username and password. Please provide them using --auth-username and --auth-password command line arguments or AUTH_USERNAME and AUTH_PASSWORD environment variables.',
|
|
144
|
+
)
|
|
145
|
+
elif config.auth_type == 'api_key':
|
|
146
|
+
handle_auth_error(
|
|
147
|
+
'api_key',
|
|
148
|
+
'API Key authentication requires a valid API key. Please provide it using --auth-api-key command line argument or AUTH_API_KEY environment variable.',
|
|
149
|
+
)
|
|
150
|
+
elif config.auth_type == 'cognito':
|
|
151
|
+
handle_auth_error(
|
|
152
|
+
'cognito',
|
|
153
|
+
'Cognito authentication requires client ID, username, and password. Please provide them using --auth-cognito-client-id, --auth-cognito-username, and --auth-cognito-password command line arguments or corresponding environment variables.',
|
|
154
|
+
)
|
|
155
|
+
else:
|
|
156
|
+
logger.warning(
|
|
157
|
+
'Continuing with incomplete authentication configuration. This may cause API requests to fail.'
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Log authentication info
|
|
161
|
+
if config.auth_type != 'none':
|
|
162
|
+
logger.info(f'Using {auth_provider.provider_name} authentication')
|
|
163
|
+
|
|
164
|
+
# Create the HTTP client with authentication and connection pooling
|
|
165
|
+
client = HttpClientFactory.create_client(
|
|
166
|
+
base_url=config.api_base_url,
|
|
167
|
+
headers=auth_headers,
|
|
168
|
+
auth=httpx_auth,
|
|
169
|
+
cookies=auth_cookies,
|
|
170
|
+
)
|
|
171
|
+
logger.info(f'Created HTTP client for API base URL: {config.api_base_url}')
|
|
172
|
+
|
|
173
|
+
custom_mappings = []
|
|
174
|
+
|
|
175
|
+
# Identify GET operations with query parameters in the OpenAPI spec
|
|
176
|
+
for path, path_item in openapi_spec.get('paths', {}).items():
|
|
177
|
+
for method, operation in path_item.items():
|
|
178
|
+
if method.lower() == 'get':
|
|
179
|
+
parameters = operation.get('parameters', [])
|
|
180
|
+
query_params = [p for p in parameters if p.get('in') == 'query']
|
|
181
|
+
if query_params:
|
|
182
|
+
# Create a specific mapping for this path to ensure it's treated as a TOOL
|
|
183
|
+
custom_mappings.append(
|
|
184
|
+
RouteMap(
|
|
185
|
+
methods=['GET'],
|
|
186
|
+
pattern=f'^{re.escape(path)}$',
|
|
187
|
+
route_type=RouteType.TOOL,
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Create the FastMCP server with custom route mappings
|
|
192
|
+
logger.info('Creating FastMCP server with OpenAPI specification')
|
|
193
|
+
# Update API name from OpenAPI spec title if available
|
|
194
|
+
if openapi_spec and isinstance(openapi_spec, dict) and 'info' in openapi_spec:
|
|
195
|
+
if 'title' in openapi_spec['info'] and openapi_spec['info']['title']:
|
|
196
|
+
config.api_name = openapi_spec['info']['title']
|
|
197
|
+
logger.info(f'Updated API name from OpenAPI spec title: {config.api_name}')
|
|
198
|
+
server = FastMCPOpenAPI(
|
|
199
|
+
openapi_spec=openapi_spec,
|
|
200
|
+
client=client,
|
|
201
|
+
name=config.api_name or 'OpenAPI MCP Server',
|
|
202
|
+
route_maps=custom_mappings, # Custom mappings take precedence over default mappings
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Log route information at debug level
|
|
206
|
+
if logger.level == 'DEBUG':
|
|
207
|
+
# Use getattr with default value to safely access attributes
|
|
208
|
+
openapi_router = getattr(server, '_openapi_router', None)
|
|
209
|
+
if openapi_router is not None:
|
|
210
|
+
routes = getattr(openapi_router, '_routes', [])
|
|
211
|
+
logger.debug(f'Server has {len(routes)} routes')
|
|
212
|
+
|
|
213
|
+
# Log details of each route
|
|
214
|
+
for i, route in enumerate(routes):
|
|
215
|
+
path = getattr(route, 'path', 'unknown')
|
|
216
|
+
method = getattr(route, 'method', 'unknown')
|
|
217
|
+
route_type = getattr(route, 'route_type', 'unknown')
|
|
218
|
+
logger.debug(f'Route {i}: {method} {path} - Type: {route_type}')
|
|
219
|
+
|
|
220
|
+
logger.info(f'Successfully configured API: {config.api_name}')
|
|
221
|
+
|
|
222
|
+
# Generate MCP-compliant prompts
|
|
223
|
+
try:
|
|
224
|
+
logger.info(f'Generating MCP prompts for API: {config.api_name}')
|
|
225
|
+
# Create prompt manager
|
|
226
|
+
prompt_manager = MCPPromptManager()
|
|
227
|
+
|
|
228
|
+
# Generate prompts
|
|
229
|
+
asyncio.run(prompt_manager.generate_prompts(server, config.api_name, openapi_spec))
|
|
230
|
+
|
|
231
|
+
# Register resource handler
|
|
232
|
+
prompt_manager.register_api_resource_handler(server, config.api_name, client)
|
|
233
|
+
|
|
234
|
+
except Exception as e:
|
|
235
|
+
logger.warning(f'Failed to generate operation-specific prompts: {e}')
|
|
236
|
+
import traceback
|
|
237
|
+
|
|
238
|
+
logger.warning(f'Traceback: {traceback.format_exc()}')
|
|
239
|
+
|
|
240
|
+
# Register health check tool
|
|
241
|
+
async def health_check() -> Dict[str, Any]:
|
|
242
|
+
"""Check the health of the server and API.
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
Dict[str, Any]: Health check results
|
|
246
|
+
|
|
247
|
+
"""
|
|
248
|
+
api_health = True
|
|
249
|
+
api_message = 'API is reachable'
|
|
250
|
+
|
|
251
|
+
# Try to make a simple request to the API
|
|
252
|
+
try:
|
|
253
|
+
# Use the retry-enabled request function
|
|
254
|
+
response = await make_request_with_retry(
|
|
255
|
+
client=client, method='GET', url='/', max_retries=2, retry_delay=0.5
|
|
256
|
+
)
|
|
257
|
+
status_code = response.status_code
|
|
258
|
+
if status_code >= 400:
|
|
259
|
+
api_health = False
|
|
260
|
+
api_message = f'API returned status code {status_code}'
|
|
261
|
+
except Exception as e:
|
|
262
|
+
api_health = False
|
|
263
|
+
api_message = f'Error connecting to API: {str(e)}'
|
|
264
|
+
|
|
265
|
+
# Get metrics summary
|
|
266
|
+
summary = metrics.get_summary()
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
'server': {
|
|
270
|
+
'status': 'healthy',
|
|
271
|
+
'version': config.version,
|
|
272
|
+
'uptime': 'N/A', # Would require tracking start time
|
|
273
|
+
},
|
|
274
|
+
'api': {
|
|
275
|
+
'name': config.api_name,
|
|
276
|
+
'status': 'healthy' if api_health else 'unhealthy',
|
|
277
|
+
'message': api_message,
|
|
278
|
+
'base_url': config.api_base_url,
|
|
279
|
+
},
|
|
280
|
+
'metrics': summary,
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
except Exception as e:
|
|
284
|
+
logger.error(f'Error setting up API: {e}')
|
|
285
|
+
logger.error('Server shutting down due to API setup error.')
|
|
286
|
+
import traceback
|
|
287
|
+
|
|
288
|
+
logger.error(f'Traceback: {traceback.format_exc()}')
|
|
289
|
+
sys.exit(1)
|
|
290
|
+
|
|
291
|
+
# Move the logging here, after the server is fully initialized
|
|
292
|
+
# Get the actual tools from the server's internal structure
|
|
293
|
+
tool_count = 0
|
|
294
|
+
tool_names = []
|
|
295
|
+
|
|
296
|
+
# Try different ways to access tools based on FastMCP implementation
|
|
297
|
+
if hasattr(server, 'list_tools'):
|
|
298
|
+
try:
|
|
299
|
+
# Use asyncio to run the async method in a synchronous context
|
|
300
|
+
tools = asyncio.run(server.list_tools()) # type: ignore
|
|
301
|
+
tool_count = len(tools)
|
|
302
|
+
tool_names = [tool.get('name') for tool in tools]
|
|
303
|
+
|
|
304
|
+
# DEBUG - Log detailed information about each tool
|
|
305
|
+
logger.debug(f'Found {tool_count} tools via list_tools()')
|
|
306
|
+
for i, tool in enumerate(tools):
|
|
307
|
+
tool_name = tool.get('name', 'unknown')
|
|
308
|
+
tool_desc = tool.get('description', 'no description')
|
|
309
|
+
logger.debug(f'Tool {i}: {tool_name} - {tool_desc}')
|
|
310
|
+
|
|
311
|
+
# Check if the tool has a schema
|
|
312
|
+
if 'parameters' in tool:
|
|
313
|
+
params = tool.get('parameters', {})
|
|
314
|
+
if 'properties' in params:
|
|
315
|
+
properties = params.get('properties', {})
|
|
316
|
+
logger.debug(f' Parameters: {list(properties.keys())}')
|
|
317
|
+
except Exception as e:
|
|
318
|
+
logger.warning(f'Failed to list tools: {e}')
|
|
319
|
+
import traceback
|
|
320
|
+
|
|
321
|
+
logger.debug(f'Tool listing error traceback: {traceback.format_exc()}')
|
|
322
|
+
|
|
323
|
+
# DEBUG - Try to access tools directly if available
|
|
324
|
+
tools = getattr(server, '_tools', {})
|
|
325
|
+
if tools:
|
|
326
|
+
logger.debug(f'Server has {len(tools)} tools in _tools attribute')
|
|
327
|
+
for tool_name, tool in tools.items():
|
|
328
|
+
logger.debug(f'Direct tool: {tool_name}')
|
|
329
|
+
|
|
330
|
+
# Log the prompt count
|
|
331
|
+
prompt_count = (
|
|
332
|
+
len(server._prompt_manager._prompts)
|
|
333
|
+
if hasattr(server, '_prompt_manager') and hasattr(server._prompt_manager, '_prompts')
|
|
334
|
+
else 0
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
# Log details of registered components
|
|
338
|
+
if tool_count > 0:
|
|
339
|
+
logger.info(f'Registered tools: {tool_names}')
|
|
340
|
+
|
|
341
|
+
if (
|
|
342
|
+
prompt_count > 0
|
|
343
|
+
and hasattr(server, '_prompt_manager')
|
|
344
|
+
and hasattr(server._prompt_manager, '_prompts')
|
|
345
|
+
):
|
|
346
|
+
prompt_names = list(server._prompt_manager._prompts.keys())
|
|
347
|
+
logger.info(f'Registered prompts: {prompt_names}')
|
|
348
|
+
|
|
349
|
+
return server
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def setup_signal_handlers():
|
|
353
|
+
"""Set up signal handlers for graceful shutdown."""
|
|
354
|
+
# Store original SIGINT handler
|
|
355
|
+
original_sigint = signal.getsignal(signal.SIGINT)
|
|
356
|
+
|
|
357
|
+
def signal_handler(sig, frame):
|
|
358
|
+
"""Handle signals by logging metrics then chain to original handler."""
|
|
359
|
+
logger.debug(f'Received signal {sig}, shutting down gracefully...')
|
|
360
|
+
|
|
361
|
+
# Log final metrics
|
|
362
|
+
summary = metrics.get_summary()
|
|
363
|
+
logger.info(f'Final metrics: {summary}')
|
|
364
|
+
|
|
365
|
+
# if sig is signal.SIGINT handle gracefully
|
|
366
|
+
if sig == signal.SIGINT:
|
|
367
|
+
logger.info('Process Interrupted, Shutting down gracefully...')
|
|
368
|
+
sys.exit(0)
|
|
369
|
+
|
|
370
|
+
# For SIGINT, chain to the original handler
|
|
371
|
+
if (
|
|
372
|
+
sig == signal.SIGINT
|
|
373
|
+
and original_sigint != signal.SIG_DFL
|
|
374
|
+
and original_sigint != signal.SIG_IGN
|
|
375
|
+
):
|
|
376
|
+
# Call the original handler
|
|
377
|
+
if callable(original_sigint):
|
|
378
|
+
original_sigint(sig, frame)
|
|
379
|
+
|
|
380
|
+
# For other signals or if no original handler, just return
|
|
381
|
+
# This lets the default handling take over
|
|
382
|
+
|
|
383
|
+
# Register for SIGTERM only
|
|
384
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
385
|
+
|
|
386
|
+
# For SIGINT, we'll use a special handler that logs then chains to original
|
|
387
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def main():
|
|
391
|
+
"""Run the MCP server with CLI argument support."""
|
|
392
|
+
parser = argparse.ArgumentParser(
|
|
393
|
+
description='This project is a server that dynamically creates Model Context Protocol (MCP) tools and resources from OpenAPI specifications. It allows Large Language Models (LLMs) to interact with APIs through the Model Context Protocol.'
|
|
394
|
+
)
|
|
395
|
+
# Server configuration
|
|
396
|
+
parser.add_argument('--port', type=int, help='Port to run the server on')
|
|
397
|
+
parser.add_argument(
|
|
398
|
+
'--log-level',
|
|
399
|
+
choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
|
|
400
|
+
default='INFO',
|
|
401
|
+
help='Set logging level',
|
|
402
|
+
)
|
|
403
|
+
parser.add_argument('--debug', action='store_true', help='Enable debug mode')
|
|
404
|
+
|
|
405
|
+
# API configuration
|
|
406
|
+
parser.add_argument('--api-name', help='Name of the API (default: petstore)')
|
|
407
|
+
parser.add_argument('--api-url', help='Base URL of the API')
|
|
408
|
+
parser.add_argument('--spec-url', help='URL of the OpenAPI specification')
|
|
409
|
+
parser.add_argument('--spec-path', help='Local path to the OpenAPI specification file')
|
|
410
|
+
|
|
411
|
+
# Authentication configuration
|
|
412
|
+
parser.add_argument(
|
|
413
|
+
'--auth-type',
|
|
414
|
+
choices=['none', 'basic', 'bearer', 'api_key', 'cognito'],
|
|
415
|
+
help='Authentication type to use (default: none)',
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
# Basic auth
|
|
419
|
+
parser.add_argument('--auth-username', help='Username for basic authentication')
|
|
420
|
+
parser.add_argument('--auth-password', help='Password for basic authentication')
|
|
421
|
+
|
|
422
|
+
# Bearer auth
|
|
423
|
+
parser.add_argument('--auth-token', help='Token for bearer authentication')
|
|
424
|
+
|
|
425
|
+
# API key auth
|
|
426
|
+
parser.add_argument('--auth-api-key', help='API key for API key authentication')
|
|
427
|
+
parser.add_argument('--auth-api-key-name', help='Name of the API key (default: api_key)')
|
|
428
|
+
parser.add_argument(
|
|
429
|
+
'--auth-api-key-in',
|
|
430
|
+
choices=['header', 'query', 'cookie'],
|
|
431
|
+
help='Where to place the API key (default: header)',
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
# Cognito auth
|
|
435
|
+
parser.add_argument('--auth-cognito-client-id', help='Client ID for Cognito authentication')
|
|
436
|
+
parser.add_argument('--auth-cognito-username', help='Username for Cognito authentication')
|
|
437
|
+
parser.add_argument('--auth-cognito-password', help='Password for Cognito authentication')
|
|
438
|
+
parser.add_argument(
|
|
439
|
+
'--auth-cognito-user-pool-id', help='User Pool ID for Cognito authentication'
|
|
440
|
+
)
|
|
441
|
+
parser.add_argument('--auth-cognito-region', help='AWS region for Cognito (default: us-east-1)')
|
|
442
|
+
|
|
443
|
+
args = parser.parse_args()
|
|
444
|
+
|
|
445
|
+
# Set up logging with loguru at specified level
|
|
446
|
+
logger.remove()
|
|
447
|
+
logger.add(lambda msg: print(msg, end=''), level=args.log_level)
|
|
448
|
+
logger.info(f'Starting server with logging level: {args.log_level}')
|
|
449
|
+
|
|
450
|
+
# Load configuration
|
|
451
|
+
logger.debug('Loading configuration from arguments and environment')
|
|
452
|
+
config = load_config(args)
|
|
453
|
+
logger.debug(f'Configuration loaded: api_name={config.api_name}, transport={config.transport}')
|
|
454
|
+
|
|
455
|
+
# Create and run the MCP server
|
|
456
|
+
logger.info('Creating MCP server')
|
|
457
|
+
mcp_server = create_mcp_server(config)
|
|
458
|
+
|
|
459
|
+
# Set up signal handlers
|
|
460
|
+
setup_signal_handlers()
|
|
461
|
+
|
|
462
|
+
try:
|
|
463
|
+
# Get counts of prompts, tools, resources, and resource templates
|
|
464
|
+
async def get_all_counts(server):
|
|
465
|
+
prompts = await server.get_prompts()
|
|
466
|
+
tools = await server.get_tools()
|
|
467
|
+
resources = await server.get_resources()
|
|
468
|
+
|
|
469
|
+
# Get resource templates if available
|
|
470
|
+
resource_templates = []
|
|
471
|
+
if hasattr(server, 'get_resource_templates'):
|
|
472
|
+
try:
|
|
473
|
+
resource_templates = await server.get_resource_templates()
|
|
474
|
+
except AttributeError as e:
|
|
475
|
+
# This is expected if the method exists but is not implemented
|
|
476
|
+
logger.debug(f'get_resource_templates exists but not implemented: {e}')
|
|
477
|
+
except Exception as e:
|
|
478
|
+
# Log other unexpected errors
|
|
479
|
+
logger.warning(f'Error retrieving resource templates: {e}')
|
|
480
|
+
|
|
481
|
+
return len(prompts), len(tools), len(resources), len(resource_templates)
|
|
482
|
+
|
|
483
|
+
prompt_count, tool_count, resource_count, resource_template_count = asyncio.run(
|
|
484
|
+
get_all_counts(mcp_server)
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
# Log all counts in a single statement
|
|
488
|
+
logger.info(
|
|
489
|
+
f'Server components: {prompt_count} prompts, {tool_count} tools, {resource_count} resources, {resource_template_count} resource templates'
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
# Check if we have at least one tool or resource
|
|
493
|
+
if tool_count == 0 and resource_count == 0:
|
|
494
|
+
logger.warning(
|
|
495
|
+
'No tools or resources were registered. This might indicate an issue with the API specification or authentication.'
|
|
496
|
+
)
|
|
497
|
+
except Exception as e:
|
|
498
|
+
logger.error(f'Error counting tools and resources: {e}')
|
|
499
|
+
logger.error('Server shutting down due to error in tool/resource registration.')
|
|
500
|
+
import traceback
|
|
501
|
+
|
|
502
|
+
logger.error(f'Traceback: {traceback.format_exc()}')
|
|
503
|
+
sys.exit(1)
|
|
504
|
+
|
|
505
|
+
# Run server with stdio transport only
|
|
506
|
+
logger.info('Running server with stdio transport')
|
|
507
|
+
mcp_server.run()
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
if __name__ == '__main__':
|
|
511
|
+
main()
|
|
@@ -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
|
+
"""Utilities for the OpenAPI MCP Server."""
|
|
15
|
+
|
|
16
|
+
from awslabs.openapi_mcp_server import logger
|
|
17
|
+
|
|
18
|
+
__all__ = ['logger']
|