awslabs.cloudwatch-mcp-server 0.0.13__tar.gz → 0.0.14__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/PKG-INFO +2 -2
  2. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/awslabs/cloudwatch_mcp_server/__init__.py +2 -1
  3. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/awslabs/cloudwatch_mcp_server/cloudwatch_metrics/tools.py +12 -2
  4. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/pyproject.toml +2 -2
  5. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/tests/cloudwatch_metrics/test_metrics_server.py +107 -3
  6. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/uv.lock +22 -5
  7. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/.gitignore +0 -0
  8. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/.python-version +0 -0
  9. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/CHANGELOG.md +0 -0
  10. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/Dockerfile +0 -0
  11. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/LICENSE +0 -0
  12. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/NOTICE +0 -0
  13. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/README.md +0 -0
  14. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/awslabs/__init__.py +0 -0
  15. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/awslabs/cloudwatch_mcp_server/cloudwatch_alarms/models.py +0 -0
  16. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/awslabs/cloudwatch_mcp_server/cloudwatch_alarms/tools.py +0 -0
  17. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/awslabs/cloudwatch_mcp_server/cloudwatch_logs/models.py +0 -0
  18. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/awslabs/cloudwatch_mcp_server/cloudwatch_logs/tools.py +0 -0
  19. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/awslabs/cloudwatch_mcp_server/cloudwatch_metrics/cloudformation_template_generator.py +0 -0
  20. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/awslabs/cloudwatch_mcp_server/cloudwatch_metrics/constants.py +0 -0
  21. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/awslabs/cloudwatch_mcp_server/cloudwatch_metrics/data/metric_metadata.json +0 -0
  22. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/awslabs/cloudwatch_mcp_server/cloudwatch_metrics/metric_analyzer.py +0 -0
  23. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/awslabs/cloudwatch_mcp_server/cloudwatch_metrics/metric_data_decomposer.py +0 -0
  24. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/awslabs/cloudwatch_mcp_server/cloudwatch_metrics/models.py +0 -0
  25. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/awslabs/cloudwatch_mcp_server/common.py +0 -0
  26. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/awslabs/cloudwatch_mcp_server/server.py +0 -0
  27. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/docker-healthcheck.sh +0 -0
  28. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/tests/cloudwatch_alarms/test_active_alarms.py +0 -0
  29. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/tests/cloudwatch_alarms/test_alarm_history.py +0 -0
  30. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/tests/cloudwatch_alarms/test_alarm_history_integration.py +0 -0
  31. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/tests/cloudwatch_alarms/test_alarms_error_handling.py +0 -0
  32. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/tests/cloudwatch_logs/test_logs_error_handling.py +0 -0
  33. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/tests/cloudwatch_logs/test_logs_models.py +0 -0
  34. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/tests/cloudwatch_logs/test_logs_server.py +0 -0
  35. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/tests/cloudwatch_metrics/test_analyze_metric.py +0 -0
  36. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/tests/cloudwatch_metrics/test_cloudformation_template_generator.py +0 -0
  37. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/tests/cloudwatch_metrics/test_decomposer_trend.py +0 -0
  38. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/tests/cloudwatch_metrics/test_metric_analyzer.py +0 -0
  39. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/tests/cloudwatch_metrics/test_metrics_error_handling.py +0 -0
  40. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/tests/cloudwatch_metrics/test_metrics_models.py +0 -0
  41. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/tests/cloudwatch_metrics/test_seasonal_detector.py +0 -0
  42. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/tests/cloudwatch_metrics/test_seasonality_enum.py +0 -0
  43. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/tests/cloudwatch_metrics/test_utils.py +0 -0
  44. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/tests/cloudwatch_metrics/test_validation_error.py +0 -0
  45. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/tests/test_common_and_server.py +0 -0
  46. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/tests/test_init.py +0 -0
  47. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/tests/test_main.py +0 -0
  48. {awslabs_cloudwatch_mcp_server-0.0.13 → awslabs_cloudwatch_mcp_server-0.0.14}/uv-requirements.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: awslabs.cloudwatch-mcp-server
3
- Version: 0.0.13
3
+ Version: 0.0.14
4
4
  Summary: An AWS Labs Model Context Protocol (MCP) server for cloudwatch
5
5
  Project-URL: homepage, https://awslabs.github.io/mcp/
6
6
  Project-URL: docs, https://awslabs.github.io/mcp/servers/cloudwatch-mcp-server/
@@ -23,7 +23,7 @@ Classifier: Programming Language :: Python :: 3.13
23
23
  Requires-Python: >=3.10
24
24
  Requires-Dist: boto3>=1.38.22
25
25
  Requires-Dist: loguru>=0.7.0
26
- Requires-Dist: mcp[cli]>=1.11.0
26
+ Requires-Dist: mcp[cli]>=1.23.0
27
27
  Requires-Dist: numpy>=2.0.0
28
28
  Requires-Dist: pandas>=2.2.3
29
29
  Requires-Dist: pydantic>=2.10.6
@@ -14,4 +14,5 @@
14
14
 
15
15
  """awslabs.cloudwatch-mcp-server"""
16
16
 
17
- MCP_SERVER_VERSION = '0.0.12'
17
+ __version__ = '0.0.14'
18
+ MCP_SERVER_VERSION = __version__
@@ -162,7 +162,10 @@ class CloudWatchMetricsTools:
162
162
  ctx: Context,
163
163
  namespace: str,
164
164
  metric_name: str,
165
- start_time: Union[str, datetime],
165
+ start_time: Annotated[
166
+ Union[str, datetime],
167
+ Field(description='The start time for the metric data query (ISO format or datetime)'),
168
+ ],
166
169
  dimensions: List[Dimension] = [],
167
170
  end_time: Annotated[
168
171
  Union[str, datetime] | None,
@@ -390,10 +393,17 @@ class CloudWatchMetricsTools:
390
393
  start_time = datetime.fromisoformat(start_time.replace('Z', '+00:00'))
391
394
 
392
395
  if end_time is None:
393
- end_time = datetime.utcnow()
396
+ end_time = datetime.now(timezone.utc)
394
397
  elif isinstance(end_time, str):
395
398
  end_time = datetime.fromisoformat(end_time.replace('Z', '+00:00'))
396
399
 
400
+ # Ensure both datetimes have timezone info for correct datetime arithmetic afterwards.
401
+ # This avoids issues when datetime is passed as naive values (without timezone)
402
+ if start_time.tzinfo is None:
403
+ start_time = start_time.replace(tzinfo=timezone.utc)
404
+ if end_time.tzinfo is None:
405
+ end_time = end_time.replace(tzinfo=timezone.utc)
406
+
397
407
  # Calculate period based on time window and target datapoints
398
408
  time_window_seconds = int((end_time - start_time).total_seconds())
399
409
  calculated_period = max(60, int(time_window_seconds / target_datapoints))
@@ -1,13 +1,13 @@
1
1
  [project]
2
2
  name = "awslabs.cloudwatch-mcp-server"
3
- version = "0.0.13"
3
+ version = "0.0.14"
4
4
  description = "An AWS Labs Model Context Protocol (MCP) server for cloudwatch"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
7
7
  dependencies = [
8
8
  "boto3>=1.38.22",
9
9
  "loguru>=0.7.0",
10
- "mcp[cli]>=1.11.0",
10
+ "mcp[cli]>=1.23.0",
11
11
  "pydantic>=2.10.6",
12
12
  "numpy>=2.0.0",
13
13
  "pandas>=2.2.3",
@@ -23,7 +23,7 @@ from awslabs.cloudwatch_mcp_server.cloudwatch_metrics.models import (
23
23
  StaticAlarmThreshold,
24
24
  )
25
25
  from awslabs.cloudwatch_mcp_server.cloudwatch_metrics.tools import CloudWatchMetricsTools
26
- from datetime import datetime
26
+ from datetime import datetime, timezone
27
27
  from moto import mock_aws
28
28
  from typing import Any
29
29
  from unittest.mock import AsyncMock, MagicMock, patch
@@ -116,8 +116,9 @@ class TestGetMetricData:
116
116
  == 'CPUUtilization'
117
117
  )
118
118
  assert call_args['MetricDataQueries'][0]['MetricStat']['Stat'] == 'Average'
119
- assert call_args['StartTime'] == start_time
120
- assert call_args['EndTime'] == end_time
119
+
120
+ assert call_args['StartTime'] == start_time.replace(tzinfo=timezone.utc)
121
+ assert call_args['EndTime'] == end_time.replace(tzinfo=timezone.utc)
121
122
  assert isinstance(result, GetMetricDataResponse)
122
123
  assert len(result.metricDataResults) == 1
123
124
  assert result.metricDataResults[0].label == 'CPUUtilization'
@@ -504,6 +505,109 @@ class TestGetMetricData:
504
505
  ctx.error.assert_called_once()
505
506
  assert 'Test exception' in ctx.error.call_args[0][0]
506
507
 
508
+ @pytest.mark.parametrize(
509
+ 'start_time,end_time,test_description',
510
+ [
511
+ # Timezone-aware start, no end (defaults to now)
512
+ (
513
+ '2023-01-01T00:00:00+00:00',
514
+ None,
515
+ 'timezone-aware start_time with None end_time (defaults to now)',
516
+ ),
517
+ # Both timezone-aware
518
+ (
519
+ '2023-01-01T00:00:00+00:00',
520
+ '2023-01-01T01:00:00+00:00',
521
+ 'both timezone-aware (ISO strings)',
522
+ ),
523
+ # Both naive datetime objects
524
+ (
525
+ datetime(2023, 1, 1, 0, 0, 0),
526
+ datetime(2023, 1, 1, 1, 0, 0),
527
+ 'both naive datetime objects',
528
+ ),
529
+ # Timezone-aware datetime objects
530
+ (
531
+ datetime(2023, 1, 1, 0, 0, 0, tzinfo=__import__('datetime').timezone.utc),
532
+ datetime(2023, 1, 1, 1, 0, 0, tzinfo=__import__('datetime').timezone.utc),
533
+ 'both timezone-aware datetime objects',
534
+ ),
535
+ # Mixed: naive start, timezone-aware end (ISO string)
536
+ (
537
+ datetime(2023, 1, 1, 0, 0, 0),
538
+ '2023-01-01T01:00:00+00:00',
539
+ 'naive datetime start with timezone-aware ISO string end',
540
+ ),
541
+ # Mixed: timezone-aware start (ISO string), naive end
542
+ (
543
+ '2023-01-01T00:00:00+00:00',
544
+ datetime(2023, 1, 1, 1, 0, 0),
545
+ 'timezone-aware ISO string start with naive datetime end',
546
+ ),
547
+ # Naive start, no end
548
+ (
549
+ datetime(2023, 1, 1, 0, 0, 0),
550
+ None,
551
+ 'naive datetime start with None end_time',
552
+ ),
553
+ # Different timezone offsets
554
+ (
555
+ '2023-01-01T00:00:00-05:00',
556
+ '2023-01-01T06:00:00+00:00',
557
+ 'different timezone offsets (EST and UTC)',
558
+ ),
559
+ # ISO string with Z notation
560
+ (
561
+ '2023-01-01T00:00:00Z',
562
+ '2023-01-01T01:00:00Z',
563
+ 'ISO strings with Z notation',
564
+ ),
565
+ ],
566
+ )
567
+ async def test_get_metric_data_with_various_datetime_formats(
568
+ self, ctx, cloudwatch_metrics_tools, start_time, end_time, test_description
569
+ ):
570
+ """Parametrized test for various datetime format combinations.
571
+
572
+ Tests all combinations of:
573
+ - Timezone-aware vs naive datetimes
574
+ - ISO strings vs datetime objects
575
+ - With and without end_time (None defaults to now)
576
+ - Different timezone offsets
577
+
578
+ This ensures the fix for timezone handling works correctly in all scenarios.
579
+ """
580
+ mock_client = MagicMock()
581
+ mock_client.get_metric_data.return_value = {
582
+ 'MetricDataResults': [
583
+ {
584
+ 'Id': 'm1',
585
+ 'Label': 'CPUUtilization',
586
+ 'StatusCode': 'Complete',
587
+ 'Timestamps': [datetime(2023, 1, 1, 0, 0, 0, tzinfo=timezone.utc)],
588
+ 'Values': [10.5],
589
+ }
590
+ ],
591
+ }
592
+
593
+ with patch.object(
594
+ cloudwatch_metrics_tools, '_get_cloudwatch_client', return_value=mock_client
595
+ ):
596
+ result = await cloudwatch_metrics_tools.get_metric_data(
597
+ ctx,
598
+ namespace='AWS/EC2',
599
+ metric_name='CPUUtilization',
600
+ start_time=start_time,
601
+ end_time=end_time,
602
+ dimensions=[Dimension(name='InstanceId', value='i-1234567890abcdef0')],
603
+ statistic='AVG',
604
+ target_datapoints=60,
605
+ )
606
+
607
+ # Should not raise an error and should return valid response
608
+ assert isinstance(result, GetMetricDataResponse), f'Failed for: {test_description}'
609
+ assert len(result.metricDataResults) == 1, f'Failed for: {test_description}'
610
+
507
611
  async def test_get_metric_metadata_found(self, ctx, cloudwatch_metrics_tools):
508
612
  """Test getting metric metadata for existing metric."""
509
613
  result = await cloudwatch_metrics_tools.get_metric_metadata(
@@ -51,7 +51,7 @@ wheels = [
51
51
 
52
52
  [[package]]
53
53
  name = "awslabs-cloudwatch-mcp-server"
54
- version = "0.0.13"
54
+ version = "0.0.14"
55
55
  source = { editable = "." }
56
56
  dependencies = [
57
57
  { name = "boto3" },
@@ -81,7 +81,7 @@ dev = [
81
81
  requires-dist = [
82
82
  { name = "boto3", specifier = ">=1.38.22" },
83
83
  { name = "loguru", specifier = ">=0.7.0" },
84
- { name = "mcp", extras = ["cli"], specifier = ">=1.11.0" },
84
+ { name = "mcp", extras = ["cli"], specifier = ">=1.23.0" },
85
85
  { name = "numpy", specifier = ">=2.0.0" },
86
86
  { name = "pandas", specifier = ">=2.2.3" },
87
87
  { name = "pydantic", specifier = ">=2.10.6" },
@@ -665,7 +665,7 @@ wheels = [
665
665
 
666
666
  [[package]]
667
667
  name = "mcp"
668
- version = "1.11.0"
668
+ version = "1.23.3"
669
669
  source = { registry = "https://pypi.org/simple" }
670
670
  dependencies = [
671
671
  { name = "anyio" },
@@ -674,15 +674,18 @@ dependencies = [
674
674
  { name = "jsonschema" },
675
675
  { name = "pydantic" },
676
676
  { name = "pydantic-settings" },
677
+ { name = "pyjwt", extra = ["crypto"] },
677
678
  { name = "python-multipart" },
678
679
  { name = "pywin32", marker = "sys_platform == 'win32'" },
679
680
  { name = "sse-starlette" },
680
681
  { name = "starlette" },
682
+ { name = "typing-extensions" },
683
+ { name = "typing-inspection" },
681
684
  { name = "uvicorn", marker = "sys_platform != 'emscripten'" },
682
685
  ]
683
- sdist = { url = "https://files.pythonhosted.org/packages/3a/f5/9506eb5578d5bbe9819ee8ba3198d0ad0e2fbe3bab8b257e4131ceb7dfb6/mcp-1.11.0.tar.gz", hash = "sha256:49a213df56bb9472ff83b3132a4825f5c8f5b120a90246f08b0dac6bedac44c8", size = 406907, upload-time = "2025-07-10T16:41:09.388Z" }
686
+ sdist = { url = "https://files.pythonhosted.org/packages/a7/a4/d06a303f45997e266f2c228081abe299bbcba216cb806128e2e49095d25f/mcp-1.23.3.tar.gz", hash = "sha256:b3b0da2cc949950ce1259c7bfc1b081905a51916fcd7c8182125b85e70825201", size = 600697, upload-time = "2025-12-09T16:04:37.351Z" }
684
687
  wheels = [
685
- { url = "https://files.pythonhosted.org/packages/92/9c/c9ca79f9c512e4113a5d07043013110bb3369fc7770040c61378c7fbcf70/mcp-1.11.0-py3-none-any.whl", hash = "sha256:58deac37f7483e4b338524b98bc949b7c2b7c33d978f5fafab5bde041c5e2595", size = 155880, upload-time = "2025-07-10T16:41:07.935Z" },
688
+ { url = "https://files.pythonhosted.org/packages/32/c6/13c1a26b47b3f3a3b480783001ada4268917c9f42d78a079c336da2e75e5/mcp-1.23.3-py3-none-any.whl", hash = "sha256:32768af4b46a1b4f7df34e2bfdf5c6011e7b63d7f1b0e321d0fdef4cd6082031", size = 231570, upload-time = "2025-12-09T16:04:35.56Z" },
686
689
  ]
687
690
 
688
691
  [package.optional-dependencies]
@@ -1130,6 +1133,20 @@ wheels = [
1130
1133
  { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" },
1131
1134
  ]
1132
1135
 
1136
+ [[package]]
1137
+ name = "pyjwt"
1138
+ version = "2.10.1"
1139
+ source = { registry = "https://pypi.org/simple" }
1140
+ sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" }
1141
+ wheels = [
1142
+ { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" },
1143
+ ]
1144
+
1145
+ [package.optional-dependencies]
1146
+ crypto = [
1147
+ { name = "cryptography" },
1148
+ ]
1149
+
1133
1150
  [[package]]
1134
1151
  name = "pyright"
1135
1152
  version = "1.1.401"