smartapi-mcp 0.1.0__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.
- smartapi_mcp/__init__.py +34 -0
- smartapi_mcp/__main__.py +10 -0
- smartapi_mcp/awslabs_server.py +392 -0
- smartapi_mcp/cli.py +139 -0
- smartapi_mcp/config.py +84 -0
- smartapi_mcp/py.typed +1 -0
- smartapi_mcp/server.py +107 -0
- smartapi_mcp/smartapi.py +112 -0
- smartapi_mcp-0.1.0.dist-info/METADATA +441 -0
- smartapi_mcp-0.1.0.dist-info/RECORD +14 -0
- smartapi_mcp-0.1.0.dist-info/WHEEL +5 -0
- smartapi_mcp-0.1.0.dist-info/entry_points.txt +2 -0
- smartapi_mcp-0.1.0.dist-info/licenses/LICENSE +201 -0
- smartapi_mcp-0.1.0.dist-info/top_level.txt +1 -0
smartapi_mcp/__init__.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SmartAPI MCP Server Package
|
|
3
|
+
|
|
4
|
+
Create MCP servers for one or multiple APIs registered in SmartAPI registry.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.0"
|
|
8
|
+
__author__ = "BioThings Team"
|
|
9
|
+
__email__ = "help@biothings.io"
|
|
10
|
+
|
|
11
|
+
# Optional imports for when dependencies are available
|
|
12
|
+
try:
|
|
13
|
+
from .server import get_mcp_server, get_merged_mcp_server, merge_mcp_servers
|
|
14
|
+
from .smartapi import (
|
|
15
|
+
PREDEFINED_API_SETS,
|
|
16
|
+
get_base_server_url,
|
|
17
|
+
get_predefined_api_set,
|
|
18
|
+
get_smartapi_ids,
|
|
19
|
+
load_api_spec,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"PREDEFINED_API_SETS",
|
|
24
|
+
"get_base_server_url",
|
|
25
|
+
"get_mcp_server",
|
|
26
|
+
"get_merged_mcp_server",
|
|
27
|
+
"get_predefined_api_set",
|
|
28
|
+
"get_smartapi_ids",
|
|
29
|
+
"load_api_spec",
|
|
30
|
+
"merge_mcp_servers",
|
|
31
|
+
]
|
|
32
|
+
except ImportError:
|
|
33
|
+
# Dependencies not available, only export version info
|
|
34
|
+
__all__ = []
|
smartapi_mcp/__main__.py
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
# The following code contains modified functions from awslabs/openapi-mcp-server
|
|
2
|
+
# implementation.
|
|
3
|
+
# The original file name is `server.py`, the following changes were made for better
|
|
4
|
+
# reuse in smartapi-mcp:
|
|
5
|
+
# * Modified `create_mcp_server` function to be async and awaitable, avoid to use
|
|
6
|
+
# asyncio.run
|
|
7
|
+
# * Extracted `get_all_counts` function out of `main` function, so it can be used
|
|
8
|
+
# in other modules
|
|
9
|
+
#
|
|
10
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
11
|
+
#
|
|
12
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
13
|
+
# you may not use this file except in compliance with the License.
|
|
14
|
+
# You may obtain a copy of the License at
|
|
15
|
+
#
|
|
16
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
17
|
+
#
|
|
18
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
19
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
20
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
21
|
+
# See the License for the specific language governing permissions and
|
|
22
|
+
# limitations under the License.
|
|
23
|
+
"""awslabs openapi MCP Server implementation."""
|
|
24
|
+
|
|
25
|
+
import re
|
|
26
|
+
import sys
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
import httpx
|
|
30
|
+
|
|
31
|
+
# Import from our modules - use direct imports from sub-modules for better patching in tests
|
|
32
|
+
from awslabs.openapi_mcp_server import logger
|
|
33
|
+
from awslabs.openapi_mcp_server.api.config import Config
|
|
34
|
+
from awslabs.openapi_mcp_server.prompts import MCPPromptManager
|
|
35
|
+
from awslabs.openapi_mcp_server.utils.http_client import (
|
|
36
|
+
HttpClientFactory,
|
|
37
|
+
make_request_with_retry,
|
|
38
|
+
)
|
|
39
|
+
from awslabs.openapi_mcp_server.utils.metrics_provider import metrics
|
|
40
|
+
from awslabs.openapi_mcp_server.utils.openapi import load_openapi_spec
|
|
41
|
+
from awslabs.openapi_mcp_server.utils.openapi_validator import validate_openapi_spec
|
|
42
|
+
from fastmcp import FastMCP
|
|
43
|
+
from fastmcp.server.openapi import FastMCPOpenAPI, RouteMap, RouteType
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def create_mcp_server(config: Config) -> FastMCP:
|
|
47
|
+
"""Create and configure the FastMCP server.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
config: Server configuration
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
FastMCP: The configured FastMCP server
|
|
54
|
+
|
|
55
|
+
"""
|
|
56
|
+
# Log environment information
|
|
57
|
+
logger.debug("Environment information:")
|
|
58
|
+
logger.debug(f"Python version: {sys.version}")
|
|
59
|
+
try:
|
|
60
|
+
logger.debug(f"HTTPX version: {httpx.__version__}")
|
|
61
|
+
except AttributeError:
|
|
62
|
+
logger.debug("HTTPX version: unknown")
|
|
63
|
+
|
|
64
|
+
logger.info("Creating FastMCP server")
|
|
65
|
+
|
|
66
|
+
# Create the FastMCP server
|
|
67
|
+
server = FastMCP(
|
|
68
|
+
"awslabs.openapi-mcp-server",
|
|
69
|
+
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.",
|
|
70
|
+
dependencies=[
|
|
71
|
+
"pydantic",
|
|
72
|
+
"loguru",
|
|
73
|
+
"httpx",
|
|
74
|
+
],
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
# Load OpenAPI spec
|
|
79
|
+
if not config.api_spec_url and not config.api_spec_path:
|
|
80
|
+
logger.error("No API spec URL or path provided")
|
|
81
|
+
raise ValueError("Either api_spec_url or api_spec_path must be provided")
|
|
82
|
+
|
|
83
|
+
logger.debug(
|
|
84
|
+
f"Loading OpenAPI spec from URL: {config.api_spec_url} or path: {config.api_spec_path}"
|
|
85
|
+
)
|
|
86
|
+
openapi_spec = load_openapi_spec(
|
|
87
|
+
url=config.api_spec_url, path=config.api_spec_path
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Validate the OpenAPI spec
|
|
91
|
+
if not validate_openapi_spec(openapi_spec):
|
|
92
|
+
logger.warning(
|
|
93
|
+
"OpenAPI specification validation failed, but continuing anyway"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Create a client for the API
|
|
97
|
+
if not config.api_base_url:
|
|
98
|
+
logger.error("No API base URL provided")
|
|
99
|
+
raise ValueError("API base URL must be provided")
|
|
100
|
+
|
|
101
|
+
# Configure authentication using the auth factory
|
|
102
|
+
from awslabs.openapi_mcp_server.auth import (
|
|
103
|
+
get_auth_provider,
|
|
104
|
+
is_auth_type_available,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Import and register the specific auth provider
|
|
108
|
+
from awslabs.openapi_mcp_server.auth.register import register_provider_by_type
|
|
109
|
+
|
|
110
|
+
# Register only the provider we need
|
|
111
|
+
if config.auth_type and config.auth_type != "none":
|
|
112
|
+
logger.debug(
|
|
113
|
+
f"Registering authentication provider for type: {config.auth_type}"
|
|
114
|
+
)
|
|
115
|
+
register_provider_by_type(config.auth_type)
|
|
116
|
+
else:
|
|
117
|
+
logger.debug("No authentication type specified, using none")
|
|
118
|
+
|
|
119
|
+
# Check if the requested auth type is available
|
|
120
|
+
if config.auth_type != "none" and not is_auth_type_available(config.auth_type):
|
|
121
|
+
logger.warning(
|
|
122
|
+
f"Authentication type {config.auth_type} is not available. Falling back to none."
|
|
123
|
+
)
|
|
124
|
+
config.auth_type = "none"
|
|
125
|
+
|
|
126
|
+
# Get the auth provider
|
|
127
|
+
auth_provider = get_auth_provider(config)
|
|
128
|
+
|
|
129
|
+
# Get authentication components
|
|
130
|
+
auth_headers = auth_provider.get_auth_headers()
|
|
131
|
+
# Get auth params (not used directly but may be needed in the future)
|
|
132
|
+
_ = auth_provider.get_auth_params()
|
|
133
|
+
auth_cookies = auth_provider.get_auth_cookies()
|
|
134
|
+
httpx_auth = auth_provider.get_httpx_auth()
|
|
135
|
+
|
|
136
|
+
# Helper function to handle authentication configuration errors
|
|
137
|
+
def handle_auth_error(auth_type, error_message):
|
|
138
|
+
"""Handle authentication configuration errors.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
auth_type: The authentication type
|
|
142
|
+
error_message: The error message to log
|
|
143
|
+
|
|
144
|
+
"""
|
|
145
|
+
logger.error(
|
|
146
|
+
f"Authentication provider {auth_provider.provider_name} is not properly configured"
|
|
147
|
+
)
|
|
148
|
+
logger.error(error_message)
|
|
149
|
+
logger.error(
|
|
150
|
+
"Server shutting down due to authentication configuration error."
|
|
151
|
+
)
|
|
152
|
+
sys.exit(1)
|
|
153
|
+
|
|
154
|
+
# Check if the provider is properly configured
|
|
155
|
+
if not auth_provider.is_configured() and config.auth_type != "none":
|
|
156
|
+
if config.auth_type == "bearer":
|
|
157
|
+
handle_auth_error(
|
|
158
|
+
"bearer",
|
|
159
|
+
"Bearer authentication requires a valid token. Please provide a token using --auth-token command line argument or AUTH_TOKEN environment variable.",
|
|
160
|
+
)
|
|
161
|
+
elif config.auth_type == "basic":
|
|
162
|
+
handle_auth_error(
|
|
163
|
+
"basic",
|
|
164
|
+
"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.",
|
|
165
|
+
)
|
|
166
|
+
elif config.auth_type == "api_key":
|
|
167
|
+
handle_auth_error(
|
|
168
|
+
"api_key",
|
|
169
|
+
"API Key authentication requires a valid API key. Please provide it using --auth-api-key command line argument or AUTH_API_KEY environment variable.",
|
|
170
|
+
)
|
|
171
|
+
elif config.auth_type == "cognito":
|
|
172
|
+
handle_auth_error(
|
|
173
|
+
"cognito",
|
|
174
|
+
"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.",
|
|
175
|
+
)
|
|
176
|
+
else:
|
|
177
|
+
logger.warning(
|
|
178
|
+
"Continuing with incomplete authentication configuration. This may cause API requests to fail."
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Log authentication info
|
|
182
|
+
if config.auth_type != "none":
|
|
183
|
+
logger.info(f"Using {auth_provider.provider_name} authentication")
|
|
184
|
+
|
|
185
|
+
# Create the HTTP client with authentication and connection pooling
|
|
186
|
+
client = HttpClientFactory.create_client(
|
|
187
|
+
base_url=config.api_base_url,
|
|
188
|
+
headers=auth_headers,
|
|
189
|
+
auth=httpx_auth,
|
|
190
|
+
cookies=auth_cookies,
|
|
191
|
+
)
|
|
192
|
+
logger.info(f"Created HTTP client for API base URL: {config.api_base_url}")
|
|
193
|
+
|
|
194
|
+
custom_mappings = []
|
|
195
|
+
|
|
196
|
+
# Identify GET operations with query parameters in the OpenAPI spec
|
|
197
|
+
for path, path_item in openapi_spec.get("paths", {}).items():
|
|
198
|
+
for method, operation in path_item.items():
|
|
199
|
+
if method.lower() == "get":
|
|
200
|
+
parameters = operation.get("parameters", [])
|
|
201
|
+
query_params = [p for p in parameters if p.get("in") == "query"]
|
|
202
|
+
if query_params:
|
|
203
|
+
# Create a specific mapping for this path to ensure it's treated as a TOOL
|
|
204
|
+
custom_mappings.append(
|
|
205
|
+
RouteMap(
|
|
206
|
+
methods=["GET"],
|
|
207
|
+
pattern=f"^{re.escape(path)}$",
|
|
208
|
+
route_type=RouteType.TOOL,
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# Create the FastMCP server with custom route mappings
|
|
213
|
+
logger.info("Creating FastMCP server with OpenAPI specification")
|
|
214
|
+
# Update API name from OpenAPI spec title if available
|
|
215
|
+
if openapi_spec and isinstance(openapi_spec, dict) and "info" in openapi_spec:
|
|
216
|
+
if "title" in openapi_spec["info"] and openapi_spec["info"]["title"]:
|
|
217
|
+
config.api_name = openapi_spec["info"]["title"]
|
|
218
|
+
logger.info(
|
|
219
|
+
f"Updated API name from OpenAPI spec title: {config.api_name}"
|
|
220
|
+
)
|
|
221
|
+
server = FastMCPOpenAPI(
|
|
222
|
+
openapi_spec=openapi_spec,
|
|
223
|
+
client=client,
|
|
224
|
+
name=config.api_name or "OpenAPI MCP Server",
|
|
225
|
+
route_maps=custom_mappings, # Custom mappings take precedence over default mappings
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# Log route information at debug level
|
|
229
|
+
if logger.level == "DEBUG":
|
|
230
|
+
# Use getattr with default value to safely access attributes
|
|
231
|
+
openapi_router = getattr(server, "_openapi_router", None)
|
|
232
|
+
if openapi_router is not None:
|
|
233
|
+
routes = getattr(openapi_router, "_routes", [])
|
|
234
|
+
logger.debug(f"Server has {len(routes)} routes")
|
|
235
|
+
|
|
236
|
+
# Log details of each route
|
|
237
|
+
for i, route in enumerate(routes):
|
|
238
|
+
path = getattr(route, "path", "unknown")
|
|
239
|
+
method = getattr(route, "method", "unknown")
|
|
240
|
+
route_type = getattr(route, "route_type", "unknown")
|
|
241
|
+
logger.debug(f"Route {i}: {method} {path} - Type: {route_type}")
|
|
242
|
+
|
|
243
|
+
logger.info(f"Successfully configured API: {config.api_name}")
|
|
244
|
+
|
|
245
|
+
# Generate MCP-compliant prompts
|
|
246
|
+
try:
|
|
247
|
+
logger.info(f"Generating MCP prompts for API: {config.api_name}")
|
|
248
|
+
# Create prompt manager
|
|
249
|
+
prompt_manager = MCPPromptManager()
|
|
250
|
+
|
|
251
|
+
# Generate prompts
|
|
252
|
+
# asyncio.run(prompt_manager.generate_prompts(server, config.api_name, openapi_spec))
|
|
253
|
+
await prompt_manager.generate_prompts(server, config.api_name, openapi_spec)
|
|
254
|
+
|
|
255
|
+
# Register resource handler
|
|
256
|
+
prompt_manager.register_api_resource_handler(
|
|
257
|
+
server, config.api_name, client
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
except Exception as e:
|
|
261
|
+
logger.warning(f"Failed to generate operation-specific prompts: {e}")
|
|
262
|
+
import traceback
|
|
263
|
+
|
|
264
|
+
logger.warning(f"Traceback: {traceback.format_exc()}")
|
|
265
|
+
|
|
266
|
+
# Register health check tool
|
|
267
|
+
async def health_check() -> dict[str, Any]:
|
|
268
|
+
"""Check the health of the server and API.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
Dict[str, Any]: Health check results
|
|
272
|
+
|
|
273
|
+
"""
|
|
274
|
+
api_health = True
|
|
275
|
+
api_message = "API is reachable"
|
|
276
|
+
|
|
277
|
+
# Try to make a simple request to the API
|
|
278
|
+
try:
|
|
279
|
+
# Use the retry-enabled request function
|
|
280
|
+
response = await make_request_with_retry(
|
|
281
|
+
client=client, method="GET", url="/", max_retries=2, retry_delay=0.5
|
|
282
|
+
)
|
|
283
|
+
status_code = response.status_code
|
|
284
|
+
if status_code >= 400:
|
|
285
|
+
api_health = False
|
|
286
|
+
api_message = f"API returned status code {status_code}"
|
|
287
|
+
except Exception as e:
|
|
288
|
+
api_health = False
|
|
289
|
+
api_message = f"Error connecting to API: {e!s}"
|
|
290
|
+
|
|
291
|
+
# Get metrics summary
|
|
292
|
+
summary = metrics.get_summary()
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
"server": {
|
|
296
|
+
"status": "healthy",
|
|
297
|
+
"version": config.version,
|
|
298
|
+
"uptime": "N/A", # Would require tracking start time
|
|
299
|
+
},
|
|
300
|
+
"api": {
|
|
301
|
+
"name": config.api_name,
|
|
302
|
+
"status": "healthy" if api_health else "unhealthy",
|
|
303
|
+
"message": api_message,
|
|
304
|
+
"base_url": config.api_base_url,
|
|
305
|
+
},
|
|
306
|
+
"metrics": summary,
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
except Exception as e:
|
|
310
|
+
logger.error(f"Error setting up API: {e}")
|
|
311
|
+
logger.error("Server shutting down due to API setup error.")
|
|
312
|
+
import traceback
|
|
313
|
+
|
|
314
|
+
logger.error(f"Traceback: {traceback.format_exc()}")
|
|
315
|
+
sys.exit(1)
|
|
316
|
+
|
|
317
|
+
# Move the logging here, after the server is fully initialized
|
|
318
|
+
# Get the actual tools from the server's internal structure
|
|
319
|
+
tool_count = 0
|
|
320
|
+
tool_names = []
|
|
321
|
+
tools = None
|
|
322
|
+
|
|
323
|
+
# Try different ways to access tools based on FastMCP implementation
|
|
324
|
+
if hasattr(server, "get_tools"):
|
|
325
|
+
try:
|
|
326
|
+
# Use asyncio to run the async method in a synchronous context
|
|
327
|
+
# tools = asyncio.run(server.list_tools()) # type: ignore
|
|
328
|
+
tools = await server.get_tools()
|
|
329
|
+
tool_count = len(tools)
|
|
330
|
+
tool_names = list(tools)
|
|
331
|
+
|
|
332
|
+
# DEBUG - Log detailed information about each tool
|
|
333
|
+
logger.debug(f"Found {tool_count} tools via get_tools()")
|
|
334
|
+
for i, tool in enumerate(tools.values()):
|
|
335
|
+
tool_name = getattr(tool, "name", "unknown")
|
|
336
|
+
tool_desc = getattr(tool, "description", "no description")
|
|
337
|
+
logger.debug(f"Tool {i}: {tool_name} - {tool_desc}")
|
|
338
|
+
|
|
339
|
+
# Check if the tool has a schema
|
|
340
|
+
if hasattr(tool, "parameters"):
|
|
341
|
+
params = getattr(tool, "parameters", {})
|
|
342
|
+
if "properties" in params:
|
|
343
|
+
properties = params.get("properties", {})
|
|
344
|
+
logger.debug(f" Parameters: {list(properties.keys())}")
|
|
345
|
+
except Exception as e:
|
|
346
|
+
logger.warning(f"Failed to list tools: {e}")
|
|
347
|
+
import traceback
|
|
348
|
+
|
|
349
|
+
logger.debug(f"Tool listing error traceback: {traceback.format_exc()}")
|
|
350
|
+
|
|
351
|
+
# Log the prompt count
|
|
352
|
+
prompt_count = (
|
|
353
|
+
len(server._prompt_manager._prompts)
|
|
354
|
+
if hasattr(server, "_prompt_manager")
|
|
355
|
+
and hasattr(server._prompt_manager, "_prompts")
|
|
356
|
+
else 0
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
# Log details of registered components
|
|
360
|
+
if tool_count > 0:
|
|
361
|
+
logger.info(f"Registered tools: {tool_names}")
|
|
362
|
+
|
|
363
|
+
if (
|
|
364
|
+
prompt_count > 0
|
|
365
|
+
and hasattr(server, "_prompt_manager")
|
|
366
|
+
and hasattr(server._prompt_manager, "_prompts")
|
|
367
|
+
):
|
|
368
|
+
prompt_names = list(server._prompt_manager._prompts.keys())
|
|
369
|
+
logger.info(f"Registered prompts: {prompt_names}")
|
|
370
|
+
|
|
371
|
+
return server
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
# Get counts of prompts, tools, resources, and resource templates
|
|
375
|
+
async def get_all_counts(server):
|
|
376
|
+
prompts = await server.get_prompts()
|
|
377
|
+
tools = await server.get_tools()
|
|
378
|
+
resources = await server.get_resources()
|
|
379
|
+
|
|
380
|
+
# Get resource templates if available
|
|
381
|
+
resource_templates = []
|
|
382
|
+
if hasattr(server, "get_resource_templates"):
|
|
383
|
+
try:
|
|
384
|
+
resource_templates = await server.get_resource_templates()
|
|
385
|
+
except AttributeError as e:
|
|
386
|
+
# This is expected if the method exists but is not implemented
|
|
387
|
+
logger.debug(f"get_resource_templates exists but not implemented: {e}")
|
|
388
|
+
except Exception as e:
|
|
389
|
+
# Log other unexpected errors
|
|
390
|
+
logger.warning(f"Error retrieving resource templates: {e}")
|
|
391
|
+
|
|
392
|
+
return len(prompts), len(tools), len(resources), len(resource_templates)
|
smartapi_mcp/cli.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command Line Interface for SmartAPI MCP Server
|
|
3
|
+
|
|
4
|
+
Provides CLI commands for running and managing the SmartAPI MCP server.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import asyncio
|
|
9
|
+
import sys
|
|
10
|
+
import traceback
|
|
11
|
+
|
|
12
|
+
from awslabs.openapi_mcp_server import logger
|
|
13
|
+
from awslabs.openapi_mcp_server.server import setup_signal_handlers
|
|
14
|
+
|
|
15
|
+
from .awslabs_server import get_all_counts
|
|
16
|
+
from .config import load_config
|
|
17
|
+
from .server import get_merged_mcp_server
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main():
|
|
21
|
+
parser = argparse.ArgumentParser(
|
|
22
|
+
description="Create MCP tools based on multiple registered SmartAPI APIs."
|
|
23
|
+
)
|
|
24
|
+
parser.add_argument(
|
|
25
|
+
"--api_set",
|
|
26
|
+
help=(
|
|
27
|
+
"the set of predefined SmartAPI APIs to include, e.g. 'biothings_core' "
|
|
28
|
+
"or 'biothings'."
|
|
29
|
+
),
|
|
30
|
+
)
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"--smartapi_id",
|
|
33
|
+
help="Pass a single SmartAPI (id) to create a MCP server.",
|
|
34
|
+
)
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"--smartapi_ids",
|
|
37
|
+
help="Pass a list of SmartAPIs (comma-separated ids) to create a MCP server.",
|
|
38
|
+
)
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"--smartapi_q",
|
|
41
|
+
help="Pass a query string for a list of SmartAPIs to create a MCP server.",
|
|
42
|
+
)
|
|
43
|
+
parser.add_argument(
|
|
44
|
+
"--smartapi_exclude_ids",
|
|
45
|
+
help=(
|
|
46
|
+
"Exclude a list of SmartAPIs (comma-separated ids) to create a MCP server."
|
|
47
|
+
),
|
|
48
|
+
)
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"--host",
|
|
51
|
+
help="The host address for the MCP server in HTTP mode. Default is localhost.",
|
|
52
|
+
)
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
"--port",
|
|
55
|
+
type=int,
|
|
56
|
+
default=8000,
|
|
57
|
+
help="The http port for the MCP server in HTTP mode. Default is 8000.",
|
|
58
|
+
)
|
|
59
|
+
parser.add_argument(
|
|
60
|
+
"--transport",
|
|
61
|
+
help="The transport mode for the MCP server, either stdio (default) or http.",
|
|
62
|
+
)
|
|
63
|
+
parser.add_argument(
|
|
64
|
+
"--server_name",
|
|
65
|
+
help='The name of the MCP server, default is "smartapi_mcp".',
|
|
66
|
+
)
|
|
67
|
+
parser.add_argument(
|
|
68
|
+
"--log-level",
|
|
69
|
+
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
|
70
|
+
default="INFO",
|
|
71
|
+
help="Set logging level",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
args = parser.parse_args()
|
|
75
|
+
|
|
76
|
+
# Set up logging with loguru at specified level
|
|
77
|
+
logger.remove()
|
|
78
|
+
logger.add(lambda msg: print(msg, end=""), level=args.log_level)
|
|
79
|
+
logger.info(f"Starting server with logging level: {args.log_level}")
|
|
80
|
+
|
|
81
|
+
# Load configuration
|
|
82
|
+
logger.debug("Loading configuration from arguments and environment")
|
|
83
|
+
config = load_config(args)
|
|
84
|
+
logger.debug("Configuration loaded.")
|
|
85
|
+
|
|
86
|
+
merged_server = asyncio.run(
|
|
87
|
+
get_merged_mcp_server(
|
|
88
|
+
smartapi_q=config.smartapi_q,
|
|
89
|
+
smartapi_id=config.smartapi_id,
|
|
90
|
+
smartapi_ids=config.smartapi_ids,
|
|
91
|
+
smartapi_exclude_ids=config.smartapi_exclude_ids,
|
|
92
|
+
api_set=config.smartapi_api_set,
|
|
93
|
+
server_name=config.server_name,
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Set up signal handlers
|
|
98
|
+
setup_signal_handlers()
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
prompt_count, tool_count, resource_count, resource_template_count = asyncio.run(
|
|
102
|
+
get_all_counts(merged_server)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Log all counts in a single statement
|
|
106
|
+
logger.info(
|
|
107
|
+
f"Server components: {prompt_count} prompts, {tool_count} tools, "
|
|
108
|
+
f"{resource_count} resources, {resource_template_count} resource templates"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Check if we have at least one tool or resource
|
|
112
|
+
if tool_count == 0 and resource_count == 0:
|
|
113
|
+
logger.warning(
|
|
114
|
+
(
|
|
115
|
+
"No tools or resources were registered. This might "
|
|
116
|
+
"indicate an issue "
|
|
117
|
+
"with the API specification or authentication."
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
except Exception as e:
|
|
121
|
+
logger.error(f"Error counting tools and resources: {e}")
|
|
122
|
+
logger.error("Server shutting down due to error in tool/resource registration.")
|
|
123
|
+
logger.error(f"Traceback: {traceback.format_exc()}")
|
|
124
|
+
sys.exit(1)
|
|
125
|
+
|
|
126
|
+
if config.transport in ["http", "sse"]:
|
|
127
|
+
# Run server with http transport only
|
|
128
|
+
logger.info(f"Running server with {config.transport} transport")
|
|
129
|
+
merged_server.run(
|
|
130
|
+
transport=config.transport, host=config.host, port=config.port
|
|
131
|
+
)
|
|
132
|
+
return
|
|
133
|
+
# Otherwise run server with stdio transport by default
|
|
134
|
+
logger.info("Running server with stdio transport")
|
|
135
|
+
merged_server.run()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
if __name__ == "__main__":
|
|
139
|
+
main()
|
smartapi_mcp/config.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from dataclasses import fields
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from awslabs.openapi_mcp_server import logger
|
|
6
|
+
from awslabs.openapi_mcp_server.api import config as _config
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Config(_config.Config):
|
|
10
|
+
"""Subclass of Config to add extra configuration options"""
|
|
11
|
+
|
|
12
|
+
smartapi_id: str = ""
|
|
13
|
+
smartapi_ids: list[str] | None = None
|
|
14
|
+
smartapi_exclude_ids: list[str] | None = None
|
|
15
|
+
smartapi_q: str = ""
|
|
16
|
+
smartapi_api_set: str = ""
|
|
17
|
+
server_name: str = "smartapi-mcp"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def load_config(args: Any = None) -> Config:
|
|
21
|
+
config = Config()
|
|
22
|
+
_cfg = _config.load_config(args)
|
|
23
|
+
for field in fields(_cfg):
|
|
24
|
+
setattr(config, field.name, getattr(_cfg, field.name))
|
|
25
|
+
|
|
26
|
+
# define the following SmartAPI-specific environment variables
|
|
27
|
+
env_vars = {
|
|
28
|
+
"SMARTAPI_ID": (lambda v: setattr(config, "smartapi_id", v)),
|
|
29
|
+
"SMARTAPI_IDS": (lambda v: setattr(config, "smartapi_ids", v)),
|
|
30
|
+
"SMARTAPI_EXCLUDE_IDS": (lambda v: setattr(config, "smartapi_exclude_ids", v)),
|
|
31
|
+
"SMARTAPI_Q": (lambda v: setattr(config, "smartapi_q", v)),
|
|
32
|
+
"SMARTAPI_API_SET": (lambda v: setattr(config, "smartapi_api_set", v)),
|
|
33
|
+
"SERVER_NAME": (lambda v: setattr(config, "server_name", v)),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Load environment variables
|
|
37
|
+
env_loaded = {}
|
|
38
|
+
for key, setter in env_vars.items():
|
|
39
|
+
if key in os.environ:
|
|
40
|
+
env_value = os.environ[key]
|
|
41
|
+
setter(env_value)
|
|
42
|
+
env_loaded[key] = env_value
|
|
43
|
+
|
|
44
|
+
if env_loaded:
|
|
45
|
+
logger.debug(
|
|
46
|
+
f"Loaded {len(env_loaded)} SmartAPI-specific environment variables: "
|
|
47
|
+
f"{', '.join(env_loaded.keys())}"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Load from arguments
|
|
51
|
+
if args:
|
|
52
|
+
if hasattr(args, "smartapi_id") and args.smartapi_id:
|
|
53
|
+
logger.debug(f"Setting SmartAPI id from arguments: {args.smartapi_id}")
|
|
54
|
+
config.smartapi_id = args.smartapi_id
|
|
55
|
+
if hasattr(args, "smartapi_ids") and args.smartapi_id:
|
|
56
|
+
logger.debug(f"Setting SmartAPI ids from arguments: {args.smartapi_ids}")
|
|
57
|
+
config.smartapi_ids = args.smartapi_ids
|
|
58
|
+
if hasattr(args, "smartapi_exclude_ids") and args.smartapi_id:
|
|
59
|
+
logger.debug(
|
|
60
|
+
"Setting excluded SmartAPI ids from arguments: {}",
|
|
61
|
+
args.smartapi_exclude_ids,
|
|
62
|
+
)
|
|
63
|
+
config.smartapi_exclude_ids = args.smartapi_exclude_ids
|
|
64
|
+
if hasattr(args, "smartapi_q") and args.smartapi_q:
|
|
65
|
+
logger.debug(f"Setting SmartAPI query from arguments: {args.smartapi_q}")
|
|
66
|
+
config.smartapi_q = args.smartapi_q
|
|
67
|
+
if hasattr(args, "api_set") and args.api_set:
|
|
68
|
+
logger.debug(
|
|
69
|
+
"Setting predefined SmartAPI API set from arguments: {}",
|
|
70
|
+
args.api_set,
|
|
71
|
+
)
|
|
72
|
+
config.smartapi_api_set = args.api_set
|
|
73
|
+
if hasattr(args, "server_name") and args.server_name:
|
|
74
|
+
logger.debug(f"Setting MCP Server name from arguments: {args.server_name}")
|
|
75
|
+
config.server_name = args.server_name
|
|
76
|
+
if hasattr(args, "transport") and args.transport:
|
|
77
|
+
logger.debug(
|
|
78
|
+
f"Setting MCP Server transport mode from arguments: {args.transport}"
|
|
79
|
+
)
|
|
80
|
+
config.transport = args.transport
|
|
81
|
+
# Log final configuration details
|
|
82
|
+
logger.info("SmartAPI Configuration loaded")
|
|
83
|
+
|
|
84
|
+
return config
|
smartapi_mcp/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Marker file for PEP 561 - indicates this package supports type hints
|