awslabs.prometheus-mcp-server 0.1.1__py3-none-any.whl → 0.2.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/prometheus_mcp_server/__init__.py +1 -1
- awslabs/prometheus_mcp_server/models.py +8 -66
- awslabs/prometheus_mcp_server/server.py +856 -418
- {awslabs_prometheus_mcp_server-0.1.1.dist-info → awslabs_prometheus_mcp_server-0.2.1.dist-info}/METADATA +55 -15
- awslabs_prometheus_mcp_server-0.2.1.dist-info/RECORD +11 -0
- awslabs_prometheus_mcp_server-0.1.1.dist-info/RECORD +0 -11
- {awslabs_prometheus_mcp_server-0.1.1.dist-info → awslabs_prometheus_mcp_server-0.2.1.dist-info}/WHEEL +0 -0
- {awslabs_prometheus_mcp_server-0.1.1.dist-info → awslabs_prometheus_mcp_server-0.2.1.dist-info}/entry_points.txt +0 -0
- {awslabs_prometheus_mcp_server-0.1.1.dist-info → awslabs_prometheus_mcp_server-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {awslabs_prometheus_mcp_server-0.1.1.dist-info → awslabs_prometheus_mcp_server-0.2.1.dist-info}/licenses/NOTICE +0 -0
|
@@ -29,25 +29,22 @@ from awslabs.prometheus_mcp_server.consts import (
|
|
|
29
29
|
DEFAULT_SERVICE_NAME,
|
|
30
30
|
ENV_AWS_PROFILE,
|
|
31
31
|
ENV_AWS_REGION,
|
|
32
|
-
ENV_AWS_SERVICE_NAME,
|
|
33
32
|
ENV_LOG_LEVEL,
|
|
34
|
-
ENV_PROMETHEUS_URL,
|
|
35
33
|
SERVER_INSTRUCTIONS,
|
|
36
34
|
)
|
|
37
35
|
from awslabs.prometheus_mcp_server.models import (
|
|
38
36
|
MetricsList,
|
|
39
|
-
PrometheusConfig,
|
|
40
37
|
ServerInfo,
|
|
41
38
|
)
|
|
42
39
|
from botocore.auth import SigV4Auth
|
|
43
40
|
from botocore.awsrequest import AWSRequest
|
|
41
|
+
from botocore.config import Config
|
|
44
42
|
from botocore.exceptions import ClientError, NoCredentialsError
|
|
45
43
|
from dotenv import load_dotenv
|
|
46
44
|
from loguru import logger
|
|
47
45
|
from mcp.server.fastmcp import Context, FastMCP
|
|
48
46
|
from pydantic import Field
|
|
49
47
|
from typing import Any, Dict, Optional
|
|
50
|
-
from urllib.parse import urlparse
|
|
51
48
|
|
|
52
49
|
|
|
53
50
|
# Configure loguru
|
|
@@ -55,347 +52,341 @@ logger.remove()
|
|
|
55
52
|
logger.add(sys.stderr, level=os.getenv(ENV_LOG_LEVEL, 'INFO'))
|
|
56
53
|
|
|
57
54
|
|
|
58
|
-
|
|
59
|
-
"""
|
|
60
|
-
parser = argparse.ArgumentParser(description='Prometheus MCP Server')
|
|
61
|
-
parser.add_argument('--profile', type=str, help='AWS profile name to use')
|
|
62
|
-
parser.add_argument('--region', type=str, help='AWS region to use')
|
|
63
|
-
parser.add_argument('--url', type=str, help='Prometheus URL')
|
|
64
|
-
parser.add_argument('--config', type=str, help='Path to configuration file')
|
|
65
|
-
parser.add_argument('--debug', action='store_true', help='Enable debug logging')
|
|
66
|
-
return parser.parse_args()
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def load_config(args):
|
|
70
|
-
"""Load configuration from file, environment variables, and command line arguments."""
|
|
71
|
-
# Load .env file if it exists
|
|
72
|
-
load_dotenv()
|
|
73
|
-
|
|
74
|
-
# Initialize config with default values
|
|
75
|
-
config_data = {
|
|
76
|
-
'aws_profile': None,
|
|
77
|
-
'aws_region': DEFAULT_AWS_REGION,
|
|
78
|
-
'prometheus_url': '',
|
|
79
|
-
'service_name': DEFAULT_SERVICE_NAME,
|
|
80
|
-
'max_retries': DEFAULT_MAX_RETRIES,
|
|
81
|
-
'retry_delay': DEFAULT_RETRY_DELAY,
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
# Load from config file if specified
|
|
85
|
-
if args.config and os.path.exists(args.config):
|
|
86
|
-
try:
|
|
87
|
-
with open(args.config, 'r') as f:
|
|
88
|
-
file_config = json.load(f)
|
|
89
|
-
config_data.update(file_config)
|
|
90
|
-
logger.info(f'Loaded configuration from {args.config}')
|
|
91
|
-
except Exception as e:
|
|
92
|
-
logger.error(f'Error loading config file: {e}')
|
|
93
|
-
|
|
94
|
-
# Override with environment variables
|
|
95
|
-
if os.getenv(ENV_AWS_PROFILE):
|
|
96
|
-
config_data['aws_profile'] = os.getenv(ENV_AWS_PROFILE)
|
|
97
|
-
if os.getenv(ENV_AWS_REGION):
|
|
98
|
-
config_data['aws_region'] = os.getenv(ENV_AWS_REGION)
|
|
99
|
-
if os.getenv(ENV_PROMETHEUS_URL):
|
|
100
|
-
config_data['prometheus_url'] = os.getenv(ENV_PROMETHEUS_URL)
|
|
101
|
-
if os.getenv(ENV_AWS_SERVICE_NAME):
|
|
102
|
-
config_data['service_name'] = os.getenv(ENV_AWS_SERVICE_NAME)
|
|
103
|
-
|
|
104
|
-
# Override with command line arguments
|
|
105
|
-
if args.profile:
|
|
106
|
-
config_data['aws_profile'] = args.profile
|
|
107
|
-
if args.region:
|
|
108
|
-
config_data['aws_region'] = args.region
|
|
109
|
-
if args.url:
|
|
110
|
-
config_data['prometheus_url'] = args.url
|
|
111
|
-
if args.debug:
|
|
112
|
-
logger.level('DEBUG')
|
|
113
|
-
logger.debug('Debug logging enabled')
|
|
114
|
-
|
|
115
|
-
return config_data
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def setup_environment(config):
|
|
119
|
-
"""Setup and validate environment variables."""
|
|
120
|
-
logger.info('Setting up environment...')
|
|
121
|
-
|
|
122
|
-
# Validate Prometheus URL
|
|
123
|
-
if not config['prometheus_url']:
|
|
124
|
-
logger.error(
|
|
125
|
-
'ERROR: Prometheus URL not configured. Please set using --url parameter or PROMETHEUS_URL environment variable.'
|
|
126
|
-
)
|
|
127
|
-
return False
|
|
55
|
+
class ConfigManager:
|
|
56
|
+
"""Configuration management for the application."""
|
|
128
57
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
58
|
+
@staticmethod
|
|
59
|
+
def parse_arguments():
|
|
60
|
+
"""Parse command line arguments."""
|
|
61
|
+
parser = argparse.ArgumentParser(description='Prometheus MCP Server')
|
|
62
|
+
parser.add_argument('--profile', type=str, help='AWS profile name to use')
|
|
63
|
+
parser.add_argument('--region', type=str, help='AWS region to use')
|
|
64
|
+
parser.add_argument('--url', type=str, help='Prometheus URL to use')
|
|
65
|
+
parser.add_argument('--debug', action='store_true', help='Enable debug logging')
|
|
66
|
+
return parser.parse_args()
|
|
135
67
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
f"WARNING: URL doesn't appear to be an AWS Managed Prometheus endpoint: {config['prometheus_url']}"
|
|
142
|
-
)
|
|
143
|
-
logger.warning(
|
|
144
|
-
'Expected format: https://aps-workspaces.[region].amazonaws.com/workspaces/ws-[id]'
|
|
145
|
-
)
|
|
146
|
-
except Exception as e:
|
|
147
|
-
logger.error(f'ERROR: Error parsing Prometheus URL: {e}')
|
|
148
|
-
return False
|
|
68
|
+
@staticmethod
|
|
69
|
+
def setup_basic_config(args):
|
|
70
|
+
"""Setup basic configuration from command line arguments and environment variables."""
|
|
71
|
+
# Load .env file if it exists
|
|
72
|
+
load_dotenv()
|
|
149
73
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
74
|
+
# Set debug logging if requested
|
|
75
|
+
if args.debug:
|
|
76
|
+
logger.level('DEBUG')
|
|
77
|
+
logger.debug('Debug logging enabled')
|
|
153
78
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
'ERROR: AWS region not configured. Please set using --region parameter or AWS_REGION environment variable.'
|
|
159
|
-
)
|
|
160
|
-
return False
|
|
161
|
-
|
|
162
|
-
logger.info(f' AWS Region: {config["aws_region"]}')
|
|
163
|
-
|
|
164
|
-
# Create session with profile if specified
|
|
165
|
-
if config['aws_profile']:
|
|
166
|
-
logger.info(f' Using AWS Profile: {config["aws_profile"]}')
|
|
167
|
-
session = boto3.Session(
|
|
168
|
-
profile_name=config['aws_profile'], region_name=config['aws_region']
|
|
169
|
-
)
|
|
170
|
-
else:
|
|
171
|
-
logger.info(' Using default AWS credentials')
|
|
172
|
-
session = boto3.Session(region_name=config['aws_region'])
|
|
173
|
-
|
|
174
|
-
credentials = session.get_credentials()
|
|
175
|
-
if credentials:
|
|
176
|
-
logger.info(' AWS Credentials: Available')
|
|
177
|
-
if credentials.token:
|
|
178
|
-
logger.info(' Credential Type: Temporary (includes session token)')
|
|
179
|
-
else:
|
|
180
|
-
logger.info(' Credential Type: Long-term')
|
|
79
|
+
# Get region, profile, and URL from args or environment
|
|
80
|
+
region = args.region or os.getenv(ENV_AWS_REGION) or DEFAULT_AWS_REGION
|
|
81
|
+
profile = args.profile or os.getenv(ENV_AWS_PROFILE)
|
|
82
|
+
url = args.url or os.getenv('PROMETHEUS_URL')
|
|
181
83
|
|
|
182
|
-
|
|
183
|
-
try:
|
|
184
|
-
sts = session.client('sts')
|
|
185
|
-
identity = sts.get_caller_identity()
|
|
186
|
-
logger.info(f' AWS Identity: {identity["Arn"]}')
|
|
187
|
-
except ClientError as e:
|
|
188
|
-
logger.warning(f'WARNING: Could not verify AWS identity: {e}')
|
|
189
|
-
logger.warning(
|
|
190
|
-
'This may indicate insufficient permissions for STS:GetCallerIdentity'
|
|
191
|
-
)
|
|
192
|
-
else:
|
|
193
|
-
logger.error('ERROR: AWS Credentials not found')
|
|
194
|
-
logger.error('Please configure AWS credentials using:')
|
|
195
|
-
logger.error(' - AWS CLI: aws configure')
|
|
196
|
-
logger.error(' - Environment variables: AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY')
|
|
197
|
-
logger.error(' - Or specify a profile with --profile')
|
|
198
|
-
return False
|
|
199
|
-
except NoCredentialsError:
|
|
200
|
-
logger.error('ERROR: AWS credentials not found')
|
|
201
|
-
logger.error('Please configure AWS credentials using:')
|
|
202
|
-
logger.error(' - AWS CLI: aws configure')
|
|
203
|
-
logger.error(' - Environment variables: AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY')
|
|
204
|
-
logger.error(' - Or specify a profile with --profile')
|
|
205
|
-
return False
|
|
206
|
-
except Exception as e:
|
|
207
|
-
logger.error(f'ERROR: Error setting up AWS session: {e}')
|
|
208
|
-
return False
|
|
84
|
+
return {'region': region, 'profile': profile, 'url': url}
|
|
209
85
|
|
|
210
|
-
return True
|
|
211
86
|
|
|
87
|
+
class AWSCredentials:
|
|
88
|
+
"""AWS credentials management."""
|
|
212
89
|
|
|
213
|
-
|
|
214
|
-
|
|
90
|
+
@staticmethod
|
|
91
|
+
def validate(region: str, profile: Optional[str] = None) -> bool:
|
|
92
|
+
"""Validate AWS credentials.
|
|
215
93
|
|
|
216
|
-
|
|
217
|
-
|
|
94
|
+
Args:
|
|
95
|
+
region: AWS region to use
|
|
96
|
+
profile: AWS profile to use (optional)
|
|
218
97
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
return True
|
|
98
|
+
Returns:
|
|
99
|
+
bool: True if credentials are valid, False otherwise
|
|
100
|
+
"""
|
|
101
|
+
logger.info('Validating AWS credentials...')
|
|
224
102
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
for key, value in params.items():
|
|
245
|
-
if not isinstance(value, str):
|
|
246
|
-
continue
|
|
247
|
-
|
|
248
|
-
for pattern in dangerous_patterns:
|
|
249
|
-
if pattern in value:
|
|
250
|
-
logger.warning(f'Potentially dangerous parameter detected: {key}={value}')
|
|
103
|
+
try:
|
|
104
|
+
# Create session with profile if specified
|
|
105
|
+
if profile:
|
|
106
|
+
logger.info(f'Using AWS Profile: {profile}')
|
|
107
|
+
session = boto3.Session(profile_name=profile, region_name=region)
|
|
108
|
+
else:
|
|
109
|
+
logger.info('Using default AWS credentials')
|
|
110
|
+
session = boto3.Session(region_name=region)
|
|
111
|
+
|
|
112
|
+
# Test AWS credentials
|
|
113
|
+
credentials = session.get_credentials()
|
|
114
|
+
if not credentials:
|
|
115
|
+
logger.error('ERROR: AWS credentials not found')
|
|
116
|
+
logger.error('Please configure AWS credentials using:')
|
|
117
|
+
logger.error(' - AWS CLI: aws configure')
|
|
118
|
+
logger.error(
|
|
119
|
+
' - Environment variables: AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY'
|
|
120
|
+
)
|
|
121
|
+
logger.error(' - Or specify a profile with --profile')
|
|
251
122
|
return False
|
|
252
123
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
max_retries: Maximum number of retry attempts
|
|
265
|
-
|
|
266
|
-
Returns:
|
|
267
|
-
The data portion of the Prometheus API response
|
|
268
|
-
|
|
269
|
-
Raises:
|
|
270
|
-
ValueError: If Prometheus URL or AWS credentials are not configured
|
|
271
|
-
RuntimeError: If the Prometheus API returns an error status
|
|
272
|
-
requests.RequestException: If there's a network or HTTP error
|
|
273
|
-
json.JSONDecodeError: If the response is not valid JSON
|
|
274
|
-
"""
|
|
275
|
-
if not config or not config.prometheus_url:
|
|
276
|
-
raise ValueError('Prometheus URL not configured')
|
|
277
|
-
|
|
278
|
-
# Validate endpoint
|
|
279
|
-
if not isinstance(endpoint, str):
|
|
280
|
-
raise ValueError('Endpoint must be a string')
|
|
124
|
+
# Test if credentials have necessary permissions
|
|
125
|
+
sts = session.client('sts', config=Config(user_agent_extra='prometheus-mcp-server'))
|
|
126
|
+
identity = sts.get_caller_identity()
|
|
127
|
+
logger.info(f'AWS Identity: {identity["Arn"]}')
|
|
128
|
+
logger.info(f'AWS Region: {region}')
|
|
129
|
+
logger.info('AWS credentials validated successfully')
|
|
130
|
+
return True
|
|
131
|
+
|
|
132
|
+
except (NoCredentialsError, ClientError) as e:
|
|
133
|
+
logger.error(f'ERROR: AWS credentials validation failed: {e}')
|
|
134
|
+
return False
|
|
281
135
|
|
|
282
|
-
if ';' in endpoint or '&&' in endpoint or '||' in endpoint:
|
|
283
|
-
raise ValueError('Invalid endpoint: potentially dangerous characters detected')
|
|
284
136
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
137
|
+
# Define dangerous patterns as a constant
|
|
138
|
+
DANGEROUS_PATTERNS = [
|
|
139
|
+
# Command injection attempts
|
|
140
|
+
';',
|
|
141
|
+
'&&',
|
|
142
|
+
'||',
|
|
143
|
+
'`',
|
|
144
|
+
'$(',
|
|
145
|
+
'${',
|
|
146
|
+
# File access attempts
|
|
147
|
+
'file://',
|
|
148
|
+
'/etc/',
|
|
149
|
+
'/var/log',
|
|
150
|
+
# Network access attempts
|
|
151
|
+
'http://',
|
|
152
|
+
'https://',
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class SecurityValidator:
|
|
157
|
+
"""Security validation utilities."""
|
|
158
|
+
|
|
159
|
+
@staticmethod
|
|
160
|
+
def validate_string(value: str, context: str = 'value') -> bool:
|
|
161
|
+
"""Validate a string for potential security issues.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
value: The string to validate
|
|
165
|
+
context: Context description for logging (e.g., 'parameter', 'query')
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
bool: True if the string is safe, False otherwise
|
|
169
|
+
"""
|
|
170
|
+
# Check for dangerous patterns
|
|
171
|
+
for pattern in DANGEROUS_PATTERNS:
|
|
172
|
+
if pattern in value:
|
|
173
|
+
logger.warning(f'Potentially dangerous {context} detected: {pattern}')
|
|
174
|
+
return False
|
|
288
175
|
|
|
289
|
-
|
|
290
|
-
base_url = config.prometheus_url
|
|
291
|
-
if not base_url.endswith(API_VERSION_PATH):
|
|
292
|
-
base_url = f'{base_url.rstrip("/")}{API_VERSION_PATH}'
|
|
176
|
+
return True
|
|
293
177
|
|
|
294
|
-
|
|
178
|
+
@staticmethod
|
|
179
|
+
def validate_params(params: Dict) -> bool:
|
|
180
|
+
"""Validate request parameters for potential security issues.
|
|
295
181
|
|
|
296
|
-
|
|
297
|
-
|
|
182
|
+
Args:
|
|
183
|
+
params: The parameters to validate
|
|
298
184
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
185
|
+
Returns:
|
|
186
|
+
bool: True if the parameters are safe, False otherwise
|
|
187
|
+
"""
|
|
188
|
+
if not params:
|
|
189
|
+
return True
|
|
304
190
|
|
|
305
|
-
|
|
191
|
+
# Check each parameter value
|
|
192
|
+
for key, value in params.items():
|
|
193
|
+
if not isinstance(value, str):
|
|
194
|
+
continue
|
|
306
195
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
method=aws_request.method,
|
|
310
|
-
url=aws_request.url,
|
|
311
|
-
headers=dict(aws_request.headers),
|
|
312
|
-
params=params or {},
|
|
313
|
-
).prepare()
|
|
196
|
+
if not SecurityValidator.validate_string(value, f'parameter {key}'):
|
|
197
|
+
return False
|
|
314
198
|
|
|
315
|
-
|
|
316
|
-
retry_count = 0
|
|
317
|
-
last_exception = None
|
|
318
|
-
retry_delay_seconds = 1 # Default retry delay if config.retry_delay is None
|
|
199
|
+
return True
|
|
319
200
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
201
|
+
@staticmethod
|
|
202
|
+
def validate_query(query: str) -> bool:
|
|
203
|
+
"""Validate a PromQL query for potential security issues.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
query: The PromQL query to validate
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
bool: True if the query is safe, False otherwise
|
|
210
|
+
"""
|
|
211
|
+
return SecurityValidator.validate_string(query, 'query pattern')
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class PrometheusClient:
|
|
215
|
+
"""Client for interacting with Prometheus API."""
|
|
216
|
+
|
|
217
|
+
@staticmethod
|
|
218
|
+
async def make_request(
|
|
219
|
+
prometheus_url: str,
|
|
220
|
+
endpoint: str,
|
|
221
|
+
params: Optional[Dict] = None,
|
|
222
|
+
region: str = DEFAULT_AWS_REGION,
|
|
223
|
+
profile: Optional[str] = None,
|
|
224
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
225
|
+
retry_delay: int = DEFAULT_RETRY_DELAY,
|
|
226
|
+
service_name: str = DEFAULT_SERVICE_NAME,
|
|
227
|
+
) -> Any:
|
|
228
|
+
"""Make a request to the Prometheus HTTP API with AWS SigV4 authentication.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
prometheus_url: The base URL for the Prometheus API
|
|
232
|
+
endpoint: The Prometheus API endpoint to call
|
|
233
|
+
params: Query parameters to include in the request
|
|
234
|
+
region: AWS region to use
|
|
235
|
+
profile: AWS profile to use
|
|
236
|
+
max_retries: Maximum number of retry attempts
|
|
237
|
+
retry_delay: Delay between retry attempts in seconds
|
|
238
|
+
service_name: AWS service name for SigV4 authentication
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
The data portion of the Prometheus API response
|
|
242
|
+
|
|
243
|
+
Raises:
|
|
244
|
+
ValueError: If Prometheus URL or AWS credentials are not configured
|
|
245
|
+
RuntimeError: If the Prometheus API returns an error status
|
|
246
|
+
requests.RequestException: If there's a network or HTTP error
|
|
247
|
+
json.JSONDecodeError: If the response is not valid JSON
|
|
248
|
+
"""
|
|
249
|
+
if not prometheus_url:
|
|
250
|
+
raise ValueError('Prometheus URL not configured')
|
|
251
|
+
|
|
252
|
+
# Validate endpoint
|
|
253
|
+
if not isinstance(endpoint, str):
|
|
254
|
+
raise ValueError('Endpoint must be a string')
|
|
255
|
+
|
|
256
|
+
if ';' in endpoint or '&&' in endpoint or '||' in endpoint:
|
|
257
|
+
raise ValueError('Invalid endpoint: potentially dangerous characters detected')
|
|
258
|
+
|
|
259
|
+
# Validate parameters
|
|
260
|
+
if params and not SecurityValidator.validate_params(params):
|
|
261
|
+
raise ValueError('Invalid parameters: potentially dangerous values detected')
|
|
262
|
+
|
|
263
|
+
# Ensure the URL ends with /api/v1
|
|
264
|
+
base_url = prometheus_url
|
|
265
|
+
if not base_url.endswith(API_VERSION_PATH):
|
|
266
|
+
base_url = f'{base_url.rstrip("/")}{API_VERSION_PATH}'
|
|
267
|
+
|
|
268
|
+
url = f'{base_url}/{endpoint.lstrip("/")}'
|
|
269
|
+
|
|
270
|
+
# Send request with retry logic
|
|
271
|
+
retry_count = 0
|
|
272
|
+
last_exception = None
|
|
273
|
+
retry_delay_seconds = retry_delay
|
|
274
|
+
|
|
275
|
+
while retry_count < max_retries:
|
|
276
|
+
try:
|
|
277
|
+
# Create a fresh session and client for each attempt
|
|
278
|
+
session = boto3.Session(profile_name=profile, region_name=region)
|
|
279
|
+
credentials = session.get_credentials()
|
|
280
|
+
if not credentials:
|
|
281
|
+
raise ValueError('AWS credentials not found')
|
|
282
|
+
|
|
283
|
+
# Create and sign the request
|
|
284
|
+
aws_request = AWSRequest(method='GET', url=url, params=params or {})
|
|
285
|
+
SigV4Auth(credentials, service_name, region).add_auth(aws_request)
|
|
286
|
+
|
|
287
|
+
# Convert to requests format
|
|
288
|
+
prepared_request = requests.Request(
|
|
289
|
+
method=aws_request.method,
|
|
290
|
+
url=aws_request.url,
|
|
291
|
+
headers=dict(aws_request.headers),
|
|
292
|
+
params=params or {},
|
|
293
|
+
).prepare()
|
|
294
|
+
|
|
295
|
+
# Send the request
|
|
296
|
+
with requests.Session() as req_session:
|
|
297
|
+
logger.debug(
|
|
298
|
+
f'Making request to {url} (attempt {retry_count + 1}/{max_retries})'
|
|
299
|
+
)
|
|
300
|
+
response = req_session.send(prepared_request)
|
|
301
|
+
response.raise_for_status()
|
|
302
|
+
data = response.json()
|
|
303
|
+
|
|
304
|
+
if data['status'] != 'success':
|
|
305
|
+
error_msg = data.get('error', 'Unknown error')
|
|
306
|
+
logger.error(f'Prometheus API request failed: {error_msg}')
|
|
307
|
+
raise RuntimeError(f'Prometheus API request failed: {error_msg}')
|
|
308
|
+
|
|
309
|
+
return data['data']
|
|
310
|
+
except (requests.RequestException, json.JSONDecodeError) as e:
|
|
311
|
+
last_exception = e
|
|
312
|
+
retry_count += 1
|
|
313
|
+
if retry_count < max_retries:
|
|
314
|
+
retry_delay_seconds = retry_delay * (
|
|
340
315
|
2 ** (retry_count - 1)
|
|
341
316
|
) # Exponential backoff
|
|
317
|
+
logger.warning(f'Request failed: {e}. Retrying in {retry_delay_seconds}s...')
|
|
318
|
+
time.sleep(retry_delay_seconds)
|
|
342
319
|
else:
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
) # Default exponential backoff
|
|
346
|
-
logger.warning(f'Request failed: {e}. Retrying in {retry_delay_seconds}s...')
|
|
347
|
-
time.sleep(retry_delay_seconds)
|
|
348
|
-
else:
|
|
349
|
-
logger.error(f'Request failed after {max_retries} attempts: {e}')
|
|
350
|
-
raise
|
|
320
|
+
logger.error(f'Request failed after {max_retries} attempts: {e}')
|
|
321
|
+
raise
|
|
351
322
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
323
|
+
if last_exception:
|
|
324
|
+
raise last_exception
|
|
325
|
+
return None
|
|
355
326
|
|
|
356
327
|
|
|
357
|
-
|
|
358
|
-
"""
|
|
328
|
+
class PrometheusConnection:
|
|
329
|
+
"""Handles Prometheus connection testing."""
|
|
359
330
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
331
|
+
@staticmethod
|
|
332
|
+
async def test_connection(
|
|
333
|
+
prometheus_url: str, region: str = DEFAULT_AWS_REGION, profile: Optional[str] = None
|
|
334
|
+
) -> bool:
|
|
335
|
+
"""Test the connection to Prometheus.
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
prometheus_url: The Prometheus URL to test
|
|
339
|
+
region: AWS region to use
|
|
340
|
+
profile: AWS profile to use
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
bool: True if connection is successful, False otherwise
|
|
344
|
+
"""
|
|
345
|
+
logger.info('Testing Prometheus connection...')
|
|
346
|
+
try:
|
|
347
|
+
# Use the PrometheusClient.make_request method
|
|
348
|
+
await PrometheusClient.make_request(
|
|
349
|
+
prometheus_url=prometheus_url,
|
|
350
|
+
endpoint='label/__name__/values',
|
|
351
|
+
params={},
|
|
352
|
+
region=region,
|
|
353
|
+
profile=profile,
|
|
354
|
+
max_retries=DEFAULT_MAX_RETRIES,
|
|
355
|
+
retry_delay=DEFAULT_RETRY_DELAY,
|
|
356
|
+
service_name=DEFAULT_SERVICE_NAME,
|
|
383
357
|
)
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
358
|
+
logger.info('Successfully connected to Prometheus!')
|
|
359
|
+
return True
|
|
360
|
+
except ClientError as e:
|
|
361
|
+
error_code = e.response.get('Error', {}).get('Code', 'Unknown')
|
|
362
|
+
if error_code == 'AccessDeniedException':
|
|
363
|
+
logger.error('ERROR: Access denied when connecting to Prometheus')
|
|
364
|
+
logger.error(
|
|
365
|
+
'Please check that your AWS credentials have the following permissions:'
|
|
366
|
+
)
|
|
367
|
+
logger.error(' - aps:QueryMetrics')
|
|
368
|
+
logger.error(' - aps:GetLabels')
|
|
369
|
+
logger.error(' - aps:GetMetricMetadata')
|
|
370
|
+
elif error_code == 'ResourceNotFoundException':
|
|
371
|
+
logger.error('ERROR: Prometheus workspace not found')
|
|
372
|
+
logger.error(
|
|
373
|
+
f'Please verify the workspace ID in your Prometheus URL: {prometheus_url}'
|
|
374
|
+
)
|
|
375
|
+
else:
|
|
376
|
+
logger.error(f'ERROR: AWS API error when connecting to Prometheus: {error_code}')
|
|
377
|
+
logger.error(f'Details: {str(e)}')
|
|
378
|
+
return False
|
|
379
|
+
except requests.RequestException as e:
|
|
380
|
+
logger.error(f'ERROR: Network error when connecting to Prometheus: {str(e)}')
|
|
381
|
+
logger.error('Please check your network connection and Prometheus URL')
|
|
382
|
+
return False
|
|
383
|
+
except Exception as e:
|
|
384
|
+
logger.error(f'ERROR: Error connecting to Prometheus: {str(e)}')
|
|
385
|
+
logger.error('Common issues:')
|
|
386
|
+
logger.error('1. Incorrect Prometheus URL')
|
|
387
|
+
logger.error('2. Missing or incorrect AWS region')
|
|
388
|
+
logger.error('3. Invalid AWS credentials or insufficient permissions')
|
|
389
|
+
return False
|
|
399
390
|
|
|
400
391
|
|
|
401
392
|
# Initialize MCP
|
|
@@ -411,73 +402,137 @@ mcp = FastMCP(
|
|
|
411
402
|
],
|
|
412
403
|
)
|
|
413
404
|
|
|
414
|
-
#
|
|
415
|
-
config = None # Will be initialized in main()
|
|
405
|
+
# No global configuration - using environment variables instead
|
|
416
406
|
|
|
417
407
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
"""Validate a PromQL query for potential security issues.
|
|
408
|
+
def get_prometheus_client(region_name: Optional[str] = None, profile_name: Optional[str] = None):
|
|
409
|
+
"""Create a boto3 AMP client using credentials from environment variables.
|
|
421
410
|
|
|
422
411
|
Args:
|
|
423
|
-
|
|
412
|
+
region_name: AWS region to use (defaults to environment variable or us-east-1)
|
|
413
|
+
profile_name: AWS profile to use (defaults to None)
|
|
424
414
|
|
|
425
415
|
Returns:
|
|
426
|
-
|
|
416
|
+
boto3 AMP client with fresh credentials
|
|
417
|
+
"""
|
|
418
|
+
# Use provided region, or get from env, or fall back to default
|
|
419
|
+
region = region_name or os.getenv('AWS_REGION') or DEFAULT_AWS_REGION
|
|
420
|
+
|
|
421
|
+
# Configure custom user agent
|
|
422
|
+
config = Config(user_agent_extra='prometheus-mcp-server')
|
|
423
|
+
|
|
424
|
+
# Create a new session to force credentials to reload
|
|
425
|
+
session = boto3.Session(profile_name=profile_name, region_name=region)
|
|
426
|
+
|
|
427
|
+
# Return AMP client
|
|
428
|
+
return session.client('amp', config=config)
|
|
429
|
+
|
|
427
430
|
|
|
428
|
-
|
|
431
|
+
async def get_workspace_details(
|
|
432
|
+
workspace_id: str, region: str = DEFAULT_AWS_REGION, profile: Optional[str] = None
|
|
433
|
+
) -> Dict[str, Any]:
|
|
434
|
+
"""Get details for a specific Prometheus workspace using DescribeWorkspace API.
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
workspace_id: The Prometheus workspace ID
|
|
438
|
+
region: AWS region where the workspace is located
|
|
439
|
+
profile: AWS profile to use (defaults to None)
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
Dictionary containing workspace details including URL from API
|
|
429
443
|
"""
|
|
430
|
-
#
|
|
431
|
-
|
|
432
|
-
# Command injection attempts
|
|
433
|
-
';',
|
|
434
|
-
'&&',
|
|
435
|
-
'||',
|
|
436
|
-
'`',
|
|
437
|
-
'$(',
|
|
438
|
-
'${',
|
|
439
|
-
# File access attempts
|
|
440
|
-
'file://',
|
|
441
|
-
'/etc/',
|
|
442
|
-
'/var/log',
|
|
443
|
-
# Network access attempts
|
|
444
|
-
'http://',
|
|
445
|
-
'https://',
|
|
446
|
-
]
|
|
447
|
-
|
|
448
|
-
# Check for dangerous patterns
|
|
449
|
-
for pattern in dangerous_patterns:
|
|
450
|
-
if pattern in query:
|
|
451
|
-
logger.warning(f'Potentially dangerous query pattern detected: {pattern}')
|
|
452
|
-
return False
|
|
444
|
+
# Get a fresh client for this request
|
|
445
|
+
aps_client = get_prometheus_client(region_name=region, profile_name=profile)
|
|
453
446
|
|
|
454
|
-
|
|
447
|
+
try:
|
|
448
|
+
# Get workspace details directly from DescribeWorkspace API
|
|
449
|
+
response = aps_client.describe_workspace(workspaceId=workspace_id)
|
|
450
|
+
workspace = response.get('workspace', {})
|
|
451
|
+
|
|
452
|
+
# Get the URL from the API response
|
|
453
|
+
prometheus_url = workspace.get('prometheusEndpoint')
|
|
454
|
+
if not prometheus_url:
|
|
455
|
+
raise ValueError(
|
|
456
|
+
f'No prometheusEndpoint found in workspace response for {workspace_id}'
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
logger.info(f'Retrieved workspace URL from DescribeWorkspace API: {prometheus_url}')
|
|
460
|
+
|
|
461
|
+
return {
|
|
462
|
+
'workspace_id': workspace_id,
|
|
463
|
+
'alias': workspace.get('alias', 'No alias'),
|
|
464
|
+
'status': workspace.get('status', {}).get('statusCode', 'UNKNOWN'),
|
|
465
|
+
'prometheus_url': prometheus_url,
|
|
466
|
+
'region': region,
|
|
467
|
+
}
|
|
468
|
+
except Exception as e:
|
|
469
|
+
logger.error(f'Error in DescribeWorkspace API: {str(e)}')
|
|
470
|
+
raise
|
|
455
471
|
|
|
456
472
|
|
|
473
|
+
# validate_query function removed - now part of SecurityValidator class
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
@mcp.tool(name='ExecuteQuery')
|
|
457
477
|
async def execute_query(
|
|
458
478
|
ctx: Context,
|
|
479
|
+
workspace_id: Optional[str] = Field(
|
|
480
|
+
None,
|
|
481
|
+
description='The Prometheus workspace ID to use (e.g., ws-12345678-abcd-1234-efgh-123456789012). Optional if a URL is configured via command line arguments.',
|
|
482
|
+
),
|
|
459
483
|
query: str = Field(..., description='The PromQL query to execute'),
|
|
460
484
|
time: Optional[str] = Field(
|
|
461
485
|
None, description='Optional timestamp for query evaluation (RFC3339 or Unix timestamp)'
|
|
462
486
|
),
|
|
487
|
+
region: Optional[str] = Field(None, description='AWS region (defaults to current region)'),
|
|
488
|
+
profile: Optional[str] = Field(None, description='AWS profile to use (defaults to None)'),
|
|
463
489
|
) -> Dict[str, Any]:
|
|
464
|
-
"""Execute
|
|
490
|
+
"""Execute a PromQL query against Amazon Managed Prometheus.
|
|
465
491
|
|
|
466
492
|
## Usage
|
|
467
493
|
- Use this tool to execute a PromQL query at a specific instant in time
|
|
468
494
|
- The query will return the current value of the specified metrics
|
|
469
495
|
- For time series data over a range, use execute_range_query instead
|
|
496
|
+
- If workspace_id is not known, use GetAvailableWorkspaces tool first to find available workspaces and ASK THE USER to choose one
|
|
497
|
+
- Uses DescribeWorkspace API to get the exact workspace URL
|
|
498
|
+
- No manual URL construction is performed
|
|
470
499
|
|
|
471
|
-
## Example
|
|
500
|
+
## Example
|
|
501
|
+
Input:
|
|
502
|
+
workspace_id: "ws-12345678-abcd-1234-efgh-123456789012"
|
|
503
|
+
query: "up"
|
|
504
|
+
region: "us-east-1"
|
|
505
|
+
|
|
506
|
+
Output:
|
|
507
|
+
{
|
|
508
|
+
"resultType": "vector",
|
|
509
|
+
"result": [
|
|
510
|
+
{
|
|
511
|
+
"metric": {"__name__": "up", "instance": "localhost:9090", "job": "prometheus"},
|
|
512
|
+
"value": [1680307200, "1"]
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
"metric": {"__name__": "up", "instance": "localhost:9100", "job": "node"},
|
|
516
|
+
"value": [1680307200, "1"]
|
|
517
|
+
}
|
|
518
|
+
]
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
Example queries:
|
|
472
522
|
- `up` - Shows which targets are up
|
|
473
523
|
- `rate(node_cpu_seconds_total{mode="system"}[1m])` - CPU usage rate
|
|
474
524
|
- `sum by(instance) (rate(node_network_receive_bytes_total[5m]))` - Network receive rate by instance
|
|
475
525
|
"""
|
|
476
526
|
try:
|
|
527
|
+
# Configure workspace using the provided workspace_id
|
|
528
|
+
workspace_config = await configure_workspace_for_request(
|
|
529
|
+
ctx, workspace_id, region, profile
|
|
530
|
+
)
|
|
531
|
+
|
|
477
532
|
logger.info(f'Executing instant query: {query}')
|
|
478
533
|
|
|
479
534
|
# Validate query for security
|
|
480
|
-
if not validate_query(query):
|
|
535
|
+
if not SecurityValidator.validate_query(query):
|
|
481
536
|
error_msg = 'Query validation failed: potentially dangerous query pattern detected'
|
|
482
537
|
logger.error(error_msg)
|
|
483
538
|
await ctx.error(error_msg)
|
|
@@ -487,11 +542,16 @@ async def execute_query(
|
|
|
487
542
|
if time:
|
|
488
543
|
params['time'] = time
|
|
489
544
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
545
|
+
return await PrometheusClient.make_request(
|
|
546
|
+
prometheus_url=workspace_config['prometheus_url'],
|
|
547
|
+
endpoint='query',
|
|
548
|
+
params=params,
|
|
549
|
+
region=workspace_config['region'],
|
|
550
|
+
profile=workspace_config['profile'],
|
|
551
|
+
max_retries=DEFAULT_MAX_RETRIES,
|
|
552
|
+
retry_delay=DEFAULT_RETRY_DELAY,
|
|
553
|
+
service_name=DEFAULT_SERVICE_NAME,
|
|
554
|
+
)
|
|
495
555
|
except Exception as e:
|
|
496
556
|
error_msg = f'Error executing query: {str(e)}'
|
|
497
557
|
logger.error(error_msg)
|
|
@@ -502,33 +562,58 @@ async def execute_query(
|
|
|
502
562
|
@mcp.tool(name='ExecuteRangeQuery')
|
|
503
563
|
async def execute_range_query(
|
|
504
564
|
ctx: Context,
|
|
565
|
+
workspace_id: Optional[str] = Field(
|
|
566
|
+
None,
|
|
567
|
+
description='The Prometheus workspace ID to use (e.g., ws-12345678-abcd-1234-efgh-123456789012). Optional if a URL is configured via command line arguments.',
|
|
568
|
+
),
|
|
505
569
|
query: str = Field(..., description='The PromQL query to execute'),
|
|
506
570
|
start: str = Field(..., description='Start timestamp (RFC3339 or Unix timestamp)'),
|
|
507
571
|
end: str = Field(..., description='End timestamp (RFC3339 or Unix timestamp)'),
|
|
508
572
|
step: str = Field(
|
|
509
573
|
..., description="Query resolution step width (duration format, e.g. '15s', '1m', '1h')"
|
|
510
574
|
),
|
|
575
|
+
region: Optional[str] = Field(None, description='AWS region (defaults to current region)'),
|
|
576
|
+
profile: Optional[str] = Field(None, description='AWS profile to use (defaults to None)'),
|
|
511
577
|
) -> Dict[str, Any]:
|
|
512
|
-
"""Execute a range query and return the result.
|
|
578
|
+
r"""Execute a range query and return the result.
|
|
513
579
|
|
|
514
580
|
## Usage
|
|
515
581
|
- Use this tool to execute a PromQL query over a time range
|
|
516
582
|
- The query will return a series of values for the specified time range
|
|
517
583
|
- Useful for generating time series data for graphs or trend analysis
|
|
584
|
+
- If workspace_id is not known, use GetAvailableWorkspaces tool first to find available workspaces and ASK THE USER to choose one
|
|
585
|
+
- Uses DescribeWorkspace API to get the exact workspace URL
|
|
586
|
+
- No manual URL construction is performed
|
|
518
587
|
|
|
519
588
|
## Example
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
589
|
+
Input:
|
|
590
|
+
workspace_id: "ws-12345678-abcd-1234-efgh-123456789012"
|
|
591
|
+
query: "rate(node_cpu_seconds_total{mode=\"system\"}[5m])"
|
|
592
|
+
start: "2023-04-01T00:00:00Z"
|
|
593
|
+
end: "2023-04-01T01:00:00Z"
|
|
594
|
+
step: "5m"
|
|
595
|
+
|
|
596
|
+
Output:
|
|
597
|
+
{
|
|
598
|
+
"resultType": "matrix",
|
|
599
|
+
"result": [
|
|
600
|
+
{
|
|
601
|
+
"metric": {"__name__": "rate", "mode": "system", "instance": "localhost:9100"},
|
|
602
|
+
"values": [[1680307200, "0.01"], [1680307500, "0.012"], ...]
|
|
603
|
+
}
|
|
604
|
+
]
|
|
605
|
+
}
|
|
526
606
|
"""
|
|
527
607
|
try:
|
|
608
|
+
# Configure workspace using the provided workspace_id
|
|
609
|
+
workspace_config = await configure_workspace_for_request(
|
|
610
|
+
ctx, workspace_id, region, profile
|
|
611
|
+
)
|
|
612
|
+
|
|
528
613
|
logger.info(f'Executing range query: {query} from {start} to {end} with step {step}')
|
|
529
614
|
|
|
530
615
|
# Validate query for security
|
|
531
|
-
if not validate_query(query):
|
|
616
|
+
if not SecurityValidator.validate_query(query):
|
|
532
617
|
error_msg = 'Query validation failed: potentially dangerous query pattern detected'
|
|
533
618
|
logger.error(error_msg)
|
|
534
619
|
await ctx.error(error_msg)
|
|
@@ -536,11 +621,16 @@ async def execute_range_query(
|
|
|
536
621
|
|
|
537
622
|
params = {'query': query, 'start': start, 'end': end, 'step': step}
|
|
538
623
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
624
|
+
return await PrometheusClient.make_request(
|
|
625
|
+
prometheus_url=workspace_config['prometheus_url'],
|
|
626
|
+
endpoint='query_range',
|
|
627
|
+
params=params,
|
|
628
|
+
region=workspace_config['region'],
|
|
629
|
+
profile=workspace_config['profile'],
|
|
630
|
+
max_retries=DEFAULT_MAX_RETRIES,
|
|
631
|
+
retry_delay=DEFAULT_RETRY_DELAY,
|
|
632
|
+
service_name=DEFAULT_SERVICE_NAME,
|
|
633
|
+
)
|
|
544
634
|
except Exception as e:
|
|
545
635
|
error_msg = f'Error executing range query: {str(e)}'
|
|
546
636
|
logger.error(error_msg)
|
|
@@ -549,28 +639,55 @@ async def execute_range_query(
|
|
|
549
639
|
|
|
550
640
|
|
|
551
641
|
@mcp.tool(name='ListMetrics')
|
|
552
|
-
async def list_metrics(
|
|
642
|
+
async def list_metrics(
|
|
643
|
+
ctx: Context,
|
|
644
|
+
workspace_id: Optional[str] = Field(
|
|
645
|
+
None,
|
|
646
|
+
description='The Prometheus workspace ID to use (e.g., ws-12345678-abcd-1234-efgh-123456789012). Optional if a URL is configured via command line arguments.',
|
|
647
|
+
),
|
|
648
|
+
region: Optional[str] = Field(None, description='AWS region (defaults to current region)'),
|
|
649
|
+
profile: Optional[str] = Field(None, description='AWS profile to use (defaults to None)'),
|
|
650
|
+
) -> MetricsList:
|
|
553
651
|
"""Get a list of all metric names.
|
|
554
652
|
|
|
555
653
|
## Usage
|
|
556
654
|
- Use this tool to discover available metrics in the Prometheus server
|
|
557
655
|
- Returns a sorted list of all metric names
|
|
558
656
|
- Useful for exploration before crafting specific queries
|
|
657
|
+
- If workspace_id is not known, use GetAvailableWorkspaces tool first to find available workspaces and ASK THE USER to choose one
|
|
559
658
|
|
|
560
659
|
## Example
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
660
|
+
Input:
|
|
661
|
+
workspace_id: "ws-12345678-abcd-1234-efgh-123456789012"
|
|
662
|
+
region: "us-east-1"
|
|
663
|
+
|
|
664
|
+
Output:
|
|
665
|
+
{
|
|
666
|
+
"metrics": [
|
|
667
|
+
"go_gc_duration_seconds",
|
|
668
|
+
"go_goroutines",
|
|
669
|
+
"http_requests_total",
|
|
670
|
+
...
|
|
671
|
+
]
|
|
672
|
+
}
|
|
565
673
|
"""
|
|
566
674
|
try:
|
|
675
|
+
# Configure workspace using the provided workspace_id
|
|
676
|
+
workspace_config = await configure_workspace_for_request(
|
|
677
|
+
ctx, workspace_id, region, profile
|
|
678
|
+
)
|
|
679
|
+
|
|
567
680
|
logger.info('Listing all available metrics')
|
|
568
|
-
max_retries = 3 # Default value
|
|
569
|
-
if config and hasattr(config, 'max_retries') and config.max_retries is not None:
|
|
570
|
-
max_retries = config.max_retries
|
|
571
681
|
|
|
572
|
-
data = await
|
|
573
|
-
'
|
|
682
|
+
data = await PrometheusClient.make_request(
|
|
683
|
+
prometheus_url=workspace_config['prometheus_url'],
|
|
684
|
+
endpoint='label/__name__/values',
|
|
685
|
+
params={},
|
|
686
|
+
region=workspace_config['region'],
|
|
687
|
+
profile=workspace_config['profile'],
|
|
688
|
+
max_retries=DEFAULT_MAX_RETRIES,
|
|
689
|
+
retry_delay=DEFAULT_RETRY_DELAY,
|
|
690
|
+
service_name=DEFAULT_SERVICE_NAME,
|
|
574
691
|
)
|
|
575
692
|
return MetricsList(metrics=sorted(data))
|
|
576
693
|
except Exception as e:
|
|
@@ -581,35 +698,51 @@ async def list_metrics(ctx: Context) -> MetricsList:
|
|
|
581
698
|
|
|
582
699
|
|
|
583
700
|
@mcp.tool(name='GetServerInfo')
|
|
584
|
-
async def get_server_info(
|
|
701
|
+
async def get_server_info(
|
|
702
|
+
ctx: Context,
|
|
703
|
+
workspace_id: Optional[str] = Field(
|
|
704
|
+
None,
|
|
705
|
+
description='The Prometheus workspace ID to use (e.g., ws-12345678-abcd-1234-efgh-123456789012). Optional if a URL is configured via command line arguments.',
|
|
706
|
+
),
|
|
707
|
+
region: Optional[str] = Field(None, description='AWS region (defaults to current region)'),
|
|
708
|
+
profile: Optional[str] = Field(None, description='AWS profile to use (defaults to None)'),
|
|
709
|
+
) -> ServerInfo:
|
|
585
710
|
"""Get information about the Prometheus server configuration.
|
|
586
711
|
|
|
587
712
|
## Usage
|
|
588
713
|
- Use this tool to retrieve the current server configuration
|
|
589
714
|
- Returns details about the Prometheus URL, AWS region, profile, and service name
|
|
590
715
|
- Useful for debugging connection issues
|
|
716
|
+
- If workspace_id is not known, use GetAvailableWorkspaces tool first to find available workspaces and ASK THE USER to choose one
|
|
717
|
+
- Uses DescribeWorkspace API to get the exact workspace URL
|
|
718
|
+
- No manual URL construction is performed
|
|
591
719
|
|
|
592
720
|
## Example
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
721
|
+
Input:
|
|
722
|
+
workspace_id: "ws-12345678-abcd-1234-efgh-123456789012"
|
|
723
|
+
region: "us-east-1"
|
|
724
|
+
|
|
725
|
+
Output:
|
|
726
|
+
{
|
|
727
|
+
"prometheus_url": "https://aps-workspaces.us-east-1.amazonaws.com/workspaces/ws-12345678-abcd-1234-efgh-123456789012",
|
|
728
|
+
"aws_region": "us-east-1",
|
|
729
|
+
"aws_profile": "default",
|
|
730
|
+
"service_name": "aps"
|
|
731
|
+
}
|
|
597
732
|
"""
|
|
598
733
|
try:
|
|
734
|
+
# Configure workspace using the provided workspace_id
|
|
735
|
+
workspace_config = await configure_workspace_for_request(
|
|
736
|
+
ctx, workspace_id, region, profile
|
|
737
|
+
)
|
|
738
|
+
|
|
599
739
|
logger.info('Retrieving server configuration information')
|
|
600
|
-
if not config:
|
|
601
|
-
return ServerInfo(
|
|
602
|
-
prometheus_url='Not configured',
|
|
603
|
-
aws_region='Not configured',
|
|
604
|
-
aws_profile='Not configured',
|
|
605
|
-
service_name='Not configured',
|
|
606
|
-
)
|
|
607
740
|
|
|
608
741
|
return ServerInfo(
|
|
609
|
-
prometheus_url=
|
|
610
|
-
aws_region=
|
|
611
|
-
aws_profile=
|
|
612
|
-
service_name=
|
|
742
|
+
prometheus_url=workspace_config['prometheus_url'],
|
|
743
|
+
aws_region=workspace_config['region'],
|
|
744
|
+
aws_profile=workspace_config['profile'] or 'default',
|
|
745
|
+
service_name=DEFAULT_SERVICE_NAME,
|
|
613
746
|
)
|
|
614
747
|
except Exception as e:
|
|
615
748
|
error_msg = f'Error retrieving server info: {str(e)}'
|
|
@@ -618,14 +751,302 @@ async def get_server_info(ctx: Context) -> ServerInfo:
|
|
|
618
751
|
raise
|
|
619
752
|
|
|
620
753
|
|
|
754
|
+
@mcp.tool(name='GetAvailableWorkspaces')
|
|
755
|
+
async def get_available_workspaces(
|
|
756
|
+
ctx: Context,
|
|
757
|
+
region: Optional[str] = Field(None, description='AWS region (defaults to current region)'),
|
|
758
|
+
profile: Optional[str] = Field(None, description='AWS profile to use (defaults to None)'),
|
|
759
|
+
) -> Dict[str, Any]:
|
|
760
|
+
"""List all available Prometheus workspaces in the specified region.
|
|
761
|
+
|
|
762
|
+
## Usage
|
|
763
|
+
- Use this tool to see all available Prometheus workspaces
|
|
764
|
+
- Shows workspace ID, alias, status, and URL for active workspaces
|
|
765
|
+
- IMPORTANT: When multiple workspaces are available, present them to the user and ask them to choose one
|
|
766
|
+
- DO NOT automatically select a workspace; always ask the user to choose when multiple options exist
|
|
767
|
+
- Uses DescribeWorkspace API to get the exact URL for each workspace
|
|
768
|
+
- No manual URL construction is performed
|
|
769
|
+
|
|
770
|
+
## Example
|
|
771
|
+
Input:
|
|
772
|
+
region: "us-east-1"
|
|
773
|
+
|
|
774
|
+
Output:
|
|
775
|
+
{
|
|
776
|
+
"workspaces": [
|
|
777
|
+
{
|
|
778
|
+
"workspace_id": "ws-12345678-abcd-1234-efgh-123456789012",
|
|
779
|
+
"alias": "production",
|
|
780
|
+
"status": "ACTIVE",
|
|
781
|
+
"prometheus_url": "https://aps-workspaces.us-east-1.amazonaws.com/workspaces/ws-12345678-abcd-1234-efgh-123456789012",
|
|
782
|
+
"is_configured": true
|
|
783
|
+
},
|
|
784
|
+
{
|
|
785
|
+
"workspace_id": "ws-87654321-dcba-4321-hgfe-210987654321",
|
|
786
|
+
"alias": "development",
|
|
787
|
+
"status": "ACTIVE",
|
|
788
|
+
"prometheus_url": "https://aps-workspaces.us-east-1.amazonaws.com/workspaces/ws-87654321-dcba-4321-hgfe-210987654321",
|
|
789
|
+
"is_configured": false
|
|
790
|
+
}
|
|
791
|
+
],
|
|
792
|
+
"count": 2,
|
|
793
|
+
"region": "us-east-1",
|
|
794
|
+
"requires_user_selection": true,
|
|
795
|
+
"configured_workspace_id": "ws-12345678-abcd-1234-efgh-123456789012"
|
|
796
|
+
}
|
|
797
|
+
"""
|
|
798
|
+
try:
|
|
799
|
+
# Use provided region or default from environment
|
|
800
|
+
aws_region = region or os.getenv('AWS_REGION') or DEFAULT_AWS_REGION
|
|
801
|
+
aws_profile = profile or os.getenv(ENV_AWS_PROFILE)
|
|
802
|
+
|
|
803
|
+
# Check if we already have a URL configured and if it contains a workspace ID
|
|
804
|
+
prometheus_url = os.getenv('PROMETHEUS_URL')
|
|
805
|
+
configured_workspace_id = None
|
|
806
|
+
if prometheus_url:
|
|
807
|
+
configured_workspace_id = extract_workspace_id_from_url(prometheus_url)
|
|
808
|
+
if configured_workspace_id:
|
|
809
|
+
logger.info(f'Found configured workspace ID in URL: {configured_workspace_id}')
|
|
810
|
+
|
|
811
|
+
logger.info(f'Listing available Prometheus workspaces in region {aws_region}')
|
|
812
|
+
|
|
813
|
+
# Get a fresh client for this request
|
|
814
|
+
aps_client = get_prometheus_client(region_name=aws_region, profile_name=aws_profile)
|
|
815
|
+
response = aps_client.list_workspaces()
|
|
816
|
+
|
|
817
|
+
workspaces = []
|
|
818
|
+
configured_workspace_details = None
|
|
819
|
+
|
|
820
|
+
for ws in response.get('workspaces', []):
|
|
821
|
+
workspace_id = ws['workspaceId']
|
|
822
|
+
|
|
823
|
+
# Only get details for active workspaces
|
|
824
|
+
if ws['status']['statusCode'] == 'ACTIVE':
|
|
825
|
+
try:
|
|
826
|
+
# Get full details including URL from DescribeWorkspace API
|
|
827
|
+
details = await get_workspace_details(workspace_id, aws_region, aws_profile)
|
|
828
|
+
|
|
829
|
+
# If this is the configured workspace, mark it
|
|
830
|
+
if configured_workspace_id and workspace_id == configured_workspace_id:
|
|
831
|
+
details['is_configured'] = True
|
|
832
|
+
details['note'] = '(Detected from URL - will be used automatically)'
|
|
833
|
+
configured_workspace_details = details
|
|
834
|
+
else:
|
|
835
|
+
details['is_configured'] = False
|
|
836
|
+
|
|
837
|
+
workspaces.append(details)
|
|
838
|
+
except Exception as e:
|
|
839
|
+
logger.warning(f'Could not get details for workspace {workspace_id}: {str(e)}')
|
|
840
|
+
# Skip this workspace if we can't get its details
|
|
841
|
+
continue
|
|
842
|
+
else:
|
|
843
|
+
# For non-active workspaces, just include basic info without URL
|
|
844
|
+
workspaces.append(
|
|
845
|
+
{
|
|
846
|
+
'workspace_id': workspace_id,
|
|
847
|
+
'alias': ws.get('alias', 'No alias'),
|
|
848
|
+
'status': ws['status']['statusCode'],
|
|
849
|
+
'region': aws_region,
|
|
850
|
+
'is_configured': configured_workspace_id
|
|
851
|
+
and workspace_id == configured_workspace_id,
|
|
852
|
+
}
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
# If we have a configured workspace but it wasn't found in the list,
|
|
856
|
+
# it might be in a different region. Add it to the list if we have details.
|
|
857
|
+
if configured_workspace_id and not configured_workspace_details and prometheus_url:
|
|
858
|
+
try:
|
|
859
|
+
# Create a basic entry for the configured workspace
|
|
860
|
+
workspaces.append(
|
|
861
|
+
{
|
|
862
|
+
'workspace_id': configured_workspace_id,
|
|
863
|
+
'alias': 'Configured Workspace',
|
|
864
|
+
'status': 'ACTIVE', # Assume active since we have a URL
|
|
865
|
+
'prometheus_url': prometheus_url,
|
|
866
|
+
'region': aws_region,
|
|
867
|
+
'is_configured': True,
|
|
868
|
+
'note': '(Detected from URL - will be used automatically)',
|
|
869
|
+
}
|
|
870
|
+
)
|
|
871
|
+
logger.info(f'Added configured workspace {configured_workspace_id} from URL')
|
|
872
|
+
except Exception as e:
|
|
873
|
+
logger.warning(f'Could not add configured workspace: {str(e)}')
|
|
874
|
+
|
|
875
|
+
# Sort workspaces to put configured workspace first
|
|
876
|
+
workspaces.sort(key=lambda ws: 0 if ws.get('is_configured') else 1)
|
|
877
|
+
|
|
878
|
+
logger.info(f'Found {len(workspaces)} workspaces in region {aws_region}')
|
|
879
|
+
|
|
880
|
+
# If we have a configured workspace ID from the URL, we don't need user selection
|
|
881
|
+
requires_selection = not configured_workspace_id and len(workspaces) > 1
|
|
882
|
+
|
|
883
|
+
message = ''
|
|
884
|
+
if configured_workspace_id:
|
|
885
|
+
message = f'A workspace ID ({configured_workspace_id}) was detected in the URL and will be used automatically. You can override it by explicitly providing a workspace_id parameter.'
|
|
886
|
+
elif len(workspaces) > 1:
|
|
887
|
+
message = 'Please choose a workspace ID to use with your queries.'
|
|
888
|
+
else:
|
|
889
|
+
message = 'Only one workspace is available. You can use it by specifying its workspace_id in your queries.'
|
|
890
|
+
|
|
891
|
+
return {
|
|
892
|
+
'workspaces': workspaces,
|
|
893
|
+
'count': len(workspaces),
|
|
894
|
+
'region': aws_region,
|
|
895
|
+
'requires_user_selection': requires_selection,
|
|
896
|
+
'configured_workspace_id': configured_workspace_id,
|
|
897
|
+
'message': message,
|
|
898
|
+
}
|
|
899
|
+
except Exception as e:
|
|
900
|
+
error_msg = f'Error listing workspaces: {str(e)}'
|
|
901
|
+
logger.error(error_msg)
|
|
902
|
+
await ctx.error(error_msg)
|
|
903
|
+
raise
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
def extract_workspace_id_from_url(url: str) -> Optional[str]:
|
|
907
|
+
"""Extract workspace ID from a Prometheus URL.
|
|
908
|
+
|
|
909
|
+
Args:
|
|
910
|
+
url: The Prometheus URL that may contain a workspace ID
|
|
911
|
+
|
|
912
|
+
Returns:
|
|
913
|
+
The extracted workspace ID or None if not found
|
|
914
|
+
"""
|
|
915
|
+
if not url:
|
|
916
|
+
return None
|
|
917
|
+
|
|
918
|
+
# Look for the pattern /workspaces/ws-XXXX in the URL
|
|
919
|
+
import re
|
|
920
|
+
|
|
921
|
+
match = re.search(r'/workspaces/(ws-[\w-]+)', url)
|
|
922
|
+
if match:
|
|
923
|
+
workspace_id = match.group(1)
|
|
924
|
+
logger.info(f'Extracted workspace ID from URL: {workspace_id}')
|
|
925
|
+
return workspace_id
|
|
926
|
+
return None
|
|
927
|
+
|
|
928
|
+
|
|
929
|
+
async def configure_workspace_for_request(
|
|
930
|
+
ctx: Context,
|
|
931
|
+
workspace_id: Optional[str] = None,
|
|
932
|
+
region: Optional[str] = None,
|
|
933
|
+
profile: Optional[str] = None,
|
|
934
|
+
) -> Dict[str, Any]:
|
|
935
|
+
"""Configure the workspace for the current request.
|
|
936
|
+
|
|
937
|
+
If a URL is provided via environment variable, it will be used directly.
|
|
938
|
+
If a workspace ID is provided, it will be used to fetch the URL from AWS API.
|
|
939
|
+
If no workspace ID is provided but the URL contains one, it will be extracted and used.
|
|
940
|
+
|
|
941
|
+
Args:
|
|
942
|
+
ctx: The MCP context
|
|
943
|
+
workspace_id: The Prometheus workspace ID to use (optional if URL contains workspace ID)
|
|
944
|
+
region: Optional AWS region (defaults to current region)
|
|
945
|
+
profile: Optional AWS profile to use
|
|
946
|
+
|
|
947
|
+
Returns:
|
|
948
|
+
Dictionary with workspace configuration including the URL
|
|
949
|
+
"""
|
|
950
|
+
try:
|
|
951
|
+
# Use provided region or default from environment
|
|
952
|
+
aws_region = region or os.getenv('AWS_REGION') or DEFAULT_AWS_REGION
|
|
953
|
+
aws_profile = profile or os.getenv(ENV_AWS_PROFILE)
|
|
954
|
+
|
|
955
|
+
# Check if we have a URL from environment
|
|
956
|
+
prometheus_url = os.getenv('PROMETHEUS_URL')
|
|
957
|
+
|
|
958
|
+
# If no workspace_id is provided, extract it from the URL if possible
|
|
959
|
+
if not workspace_id and prometheus_url:
|
|
960
|
+
extracted_workspace_id = extract_workspace_id_from_url(prometheus_url)
|
|
961
|
+
if extracted_workspace_id:
|
|
962
|
+
workspace_id = extracted_workspace_id
|
|
963
|
+
logger.info(f'Using workspace ID extracted from URL: {workspace_id}')
|
|
964
|
+
|
|
965
|
+
# If we have a URL but no workspace_id could be extracted, use the URL directly
|
|
966
|
+
if prometheus_url:
|
|
967
|
+
logger.info(f'Using Prometheus URL from environment: {prometheus_url}')
|
|
968
|
+
|
|
969
|
+
# Test connection with the URL
|
|
970
|
+
if not await PrometheusConnection.test_connection(
|
|
971
|
+
prometheus_url, aws_region, aws_profile
|
|
972
|
+
):
|
|
973
|
+
error_msg = f'Failed to connect to Prometheus with configured URL {prometheus_url}'
|
|
974
|
+
logger.error(error_msg)
|
|
975
|
+
await ctx.error(error_msg)
|
|
976
|
+
raise RuntimeError(error_msg)
|
|
977
|
+
|
|
978
|
+
return {
|
|
979
|
+
'prometheus_url': prometheus_url,
|
|
980
|
+
'region': aws_region,
|
|
981
|
+
'profile': aws_profile,
|
|
982
|
+
'workspace_id': workspace_id,
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
# If no URL is configured, require workspace_id
|
|
986
|
+
if not workspace_id:
|
|
987
|
+
error_msg = 'Workspace ID is required when no Prometheus URL is configured. Please use GetAvailableWorkspaces to list available workspaces and choose one.'
|
|
988
|
+
logger.error(error_msg)
|
|
989
|
+
await ctx.error(error_msg)
|
|
990
|
+
raise ValueError(error_msg)
|
|
991
|
+
|
|
992
|
+
logger.info(f'Configuring workspace ID for request: {workspace_id}')
|
|
993
|
+
|
|
994
|
+
# Validate workspace ID format
|
|
995
|
+
if not workspace_id.startswith('ws-'):
|
|
996
|
+
logger.warning(
|
|
997
|
+
f'Workspace ID "{workspace_id}" does not start with "ws-", which is unusual'
|
|
998
|
+
)
|
|
999
|
+
|
|
1000
|
+
# Get workspace details from DescribeWorkspace API
|
|
1001
|
+
workspace_details = await get_workspace_details(workspace_id, aws_region, aws_profile)
|
|
1002
|
+
prometheus_url = workspace_details['prometheus_url']
|
|
1003
|
+
logger.info(f'Using Prometheus URL from DescribeWorkspace API: {prometheus_url}')
|
|
1004
|
+
|
|
1005
|
+
# Test connection with the URL
|
|
1006
|
+
if not await PrometheusConnection.test_connection(prometheus_url, aws_region, aws_profile):
|
|
1007
|
+
error_msg = f'Failed to connect to Prometheus with workspace ID {workspace_id}'
|
|
1008
|
+
logger.error(error_msg)
|
|
1009
|
+
await ctx.error(error_msg)
|
|
1010
|
+
raise RuntimeError(error_msg)
|
|
1011
|
+
|
|
1012
|
+
logger.info(f'Successfully configured workspace {workspace_id} for request')
|
|
1013
|
+
|
|
1014
|
+
# Return workspace configuration
|
|
1015
|
+
return {
|
|
1016
|
+
'prometheus_url': prometheus_url,
|
|
1017
|
+
'region': aws_region,
|
|
1018
|
+
'profile': aws_profile,
|
|
1019
|
+
'workspace_id': workspace_id,
|
|
1020
|
+
}
|
|
1021
|
+
except Exception as e:
|
|
1022
|
+
error_msg = f'Error configuring workspace: {str(e)}'
|
|
1023
|
+
logger.error(error_msg)
|
|
1024
|
+
await ctx.error(error_msg)
|
|
1025
|
+
raise
|
|
1026
|
+
|
|
1027
|
+
|
|
621
1028
|
async def async_main():
|
|
622
1029
|
"""Run the async initialization tasks."""
|
|
623
|
-
#
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
1030
|
+
# Check if URL is configured in environment
|
|
1031
|
+
prometheus_url = os.getenv('PROMETHEUS_URL')
|
|
1032
|
+
if prometheus_url:
|
|
1033
|
+
logger.info(f'Using Prometheus URL from environment: {prometheus_url}')
|
|
1034
|
+
|
|
1035
|
+
# Check if the URL contains a workspace ID
|
|
1036
|
+
workspace_id = extract_workspace_id_from_url(prometheus_url)
|
|
1037
|
+
if workspace_id:
|
|
1038
|
+
logger.info(f'Detected workspace ID in URL: {workspace_id}')
|
|
1039
|
+
logger.info(
|
|
1040
|
+
'This workspace ID can be used with queries, but must be explicitly provided'
|
|
1041
|
+
)
|
|
1042
|
+
else:
|
|
1043
|
+
logger.info('No workspace ID detected in URL')
|
|
627
1044
|
|
|
628
|
-
|
|
1045
|
+
logger.info('Workspace ID will be required for each tool invocation')
|
|
1046
|
+
else:
|
|
1047
|
+
logger.info(
|
|
1048
|
+
'Initializing Prometheus MCP Server - workspace ID will be required for each tool invocation'
|
|
1049
|
+
)
|
|
629
1050
|
|
|
630
1051
|
|
|
631
1052
|
def main():
|
|
@@ -633,25 +1054,36 @@ def main():
|
|
|
633
1054
|
logger.info('Starting Prometheus MCP Server...')
|
|
634
1055
|
|
|
635
1056
|
# Parse arguments
|
|
636
|
-
args = parse_arguments()
|
|
637
|
-
|
|
638
|
-
#
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
#
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
1057
|
+
args = ConfigManager.parse_arguments()
|
|
1058
|
+
|
|
1059
|
+
# Setup basic configuration
|
|
1060
|
+
config = ConfigManager.setup_basic_config(args)
|
|
1061
|
+
|
|
1062
|
+
# Set as environment variables for other functions to use
|
|
1063
|
+
if config['url']:
|
|
1064
|
+
os.environ['PROMETHEUS_URL'] = config['url']
|
|
1065
|
+
if config['region']:
|
|
1066
|
+
os.environ['AWS_REGION'] = config['region']
|
|
1067
|
+
if config['profile']:
|
|
1068
|
+
os.environ[ENV_AWS_PROFILE] = config['profile']
|
|
1069
|
+
|
|
1070
|
+
if config['url']:
|
|
1071
|
+
logger.info(f'Using configured Prometheus URL: {config["url"]}')
|
|
1072
|
+
|
|
1073
|
+
# Check if the URL contains a workspace ID
|
|
1074
|
+
workspace_id = extract_workspace_id_from_url(config['url'])
|
|
1075
|
+
if workspace_id:
|
|
1076
|
+
logger.info(f'Detected workspace ID in URL: {workspace_id}')
|
|
1077
|
+
logger.info(
|
|
1078
|
+
'This workspace will be used automatically when no workspace ID is provided'
|
|
1079
|
+
)
|
|
1080
|
+
else:
|
|
1081
|
+
logger.info('No workspace ID detected in URL')
|
|
1082
|
+
logger.info('Workspace ID will be required for each tool invocation')
|
|
1083
|
+
|
|
1084
|
+
# Validate AWS credentials
|
|
1085
|
+
if not AWSCredentials.validate(config['region'], config['profile']):
|
|
1086
|
+
logger.error('AWS credentials validation failed')
|
|
655
1087
|
sys.exit(1)
|
|
656
1088
|
|
|
657
1089
|
# Run async initialization in an event loop
|
|
@@ -660,8 +1092,14 @@ def main():
|
|
|
660
1092
|
asyncio.run(async_main())
|
|
661
1093
|
|
|
662
1094
|
logger.info('Starting server...')
|
|
1095
|
+
|
|
663
1096
|
# Run with stdio transport
|
|
664
|
-
|
|
1097
|
+
try:
|
|
1098
|
+
logger.info('Starting with stdio transport...')
|
|
1099
|
+
mcp.run(transport='stdio')
|
|
1100
|
+
except Exception as e:
|
|
1101
|
+
logger.error(f'Error starting server with stdio transport: {e}')
|
|
1102
|
+
sys.exit(1)
|
|
665
1103
|
|
|
666
1104
|
|
|
667
1105
|
if __name__ == '__main__':
|