awslabs.cloudwatch-appsignals-mcp-server 0.1.8__tar.gz → 0.1.10__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 (37) hide show
  1. {awslabs_cloudwatch_appsignals_mcp_server-0.1.8 → awslabs_cloudwatch_appsignals_mcp_server-0.1.10}/Dockerfile +2 -2
  2. {awslabs_cloudwatch_appsignals_mcp_server-0.1.8 → awslabs_cloudwatch_appsignals_mcp_server-0.1.10}/PKG-INFO +2 -2
  3. {awslabs_cloudwatch_appsignals_mcp_server-0.1.8 → awslabs_cloudwatch_appsignals_mcp_server-0.1.10}/awslabs/cloudwatch_appsignals_mcp_server/__init__.py +1 -1
  4. {awslabs_cloudwatch_appsignals_mcp_server-0.1.8 → awslabs_cloudwatch_appsignals_mcp_server-0.1.10}/awslabs/cloudwatch_appsignals_mcp_server/trace_tools.py +72 -14
  5. {awslabs_cloudwatch_appsignals_mcp_server-0.1.8 → awslabs_cloudwatch_appsignals_mcp_server-0.1.10}/pyproject.toml +2 -2
  6. {awslabs_cloudwatch_appsignals_mcp_server-0.1.8 → awslabs_cloudwatch_appsignals_mcp_server-0.1.10}/tests/test_server.py +155 -4
  7. {awslabs_cloudwatch_appsignals_mcp_server-0.1.8 → awslabs_cloudwatch_appsignals_mcp_server-0.1.10}/uv.lock +11 -11
  8. {awslabs_cloudwatch_appsignals_mcp_server-0.1.8 → awslabs_cloudwatch_appsignals_mcp_server-0.1.10}/.gitignore +0 -0
  9. {awslabs_cloudwatch_appsignals_mcp_server-0.1.8 → awslabs_cloudwatch_appsignals_mcp_server-0.1.10}/.python-version +0 -0
  10. {awslabs_cloudwatch_appsignals_mcp_server-0.1.8 → awslabs_cloudwatch_appsignals_mcp_server-0.1.10}/CHANGELOG.md +0 -0
  11. {awslabs_cloudwatch_appsignals_mcp_server-0.1.8 → awslabs_cloudwatch_appsignals_mcp_server-0.1.10}/LICENSE +0 -0
  12. {awslabs_cloudwatch_appsignals_mcp_server-0.1.8 → awslabs_cloudwatch_appsignals_mcp_server-0.1.10}/NOTICE +0 -0
  13. {awslabs_cloudwatch_appsignals_mcp_server-0.1.8 → awslabs_cloudwatch_appsignals_mcp_server-0.1.10}/README.md +0 -0
  14. {awslabs_cloudwatch_appsignals_mcp_server-0.1.8 → awslabs_cloudwatch_appsignals_mcp_server-0.1.10}/awslabs/__init__.py +0 -0
  15. {awslabs_cloudwatch_appsignals_mcp_server-0.1.8 → awslabs_cloudwatch_appsignals_mcp_server-0.1.10}/awslabs/cloudwatch_appsignals_mcp_server/audit_presentation_utils.py +0 -0
  16. {awslabs_cloudwatch_appsignals_mcp_server-0.1.8 → awslabs_cloudwatch_appsignals_mcp_server-0.1.10}/awslabs/cloudwatch_appsignals_mcp_server/audit_utils.py +0 -0
  17. {awslabs_cloudwatch_appsignals_mcp_server-0.1.8 → awslabs_cloudwatch_appsignals_mcp_server-0.1.10}/awslabs/cloudwatch_appsignals_mcp_server/aws_clients.py +0 -0
  18. {awslabs_cloudwatch_appsignals_mcp_server-0.1.8 → awslabs_cloudwatch_appsignals_mcp_server-0.1.10}/awslabs/cloudwatch_appsignals_mcp_server/server.py +0 -0
  19. {awslabs_cloudwatch_appsignals_mcp_server-0.1.8 → awslabs_cloudwatch_appsignals_mcp_server-0.1.10}/awslabs/cloudwatch_appsignals_mcp_server/service_audit_utils.py +0 -0
  20. {awslabs_cloudwatch_appsignals_mcp_server-0.1.8 → awslabs_cloudwatch_appsignals_mcp_server-0.1.10}/awslabs/cloudwatch_appsignals_mcp_server/service_tools.py +0 -0
  21. {awslabs_cloudwatch_appsignals_mcp_server-0.1.8 → awslabs_cloudwatch_appsignals_mcp_server-0.1.10}/awslabs/cloudwatch_appsignals_mcp_server/sli_report_client.py +0 -0
  22. {awslabs_cloudwatch_appsignals_mcp_server-0.1.8 → awslabs_cloudwatch_appsignals_mcp_server-0.1.10}/awslabs/cloudwatch_appsignals_mcp_server/slo_tools.py +0 -0
  23. {awslabs_cloudwatch_appsignals_mcp_server-0.1.8 → awslabs_cloudwatch_appsignals_mcp_server-0.1.10}/awslabs/cloudwatch_appsignals_mcp_server/utils.py +0 -0
  24. {awslabs_cloudwatch_appsignals_mcp_server-0.1.8 → awslabs_cloudwatch_appsignals_mcp_server-0.1.10}/docker-healthcheck.sh +0 -0
  25. {awslabs_cloudwatch_appsignals_mcp_server-0.1.8 → awslabs_cloudwatch_appsignals_mcp_server-0.1.10}/tests/conftest.py +0 -0
  26. {awslabs_cloudwatch_appsignals_mcp_server-0.1.8 → awslabs_cloudwatch_appsignals_mcp_server-0.1.10}/tests/test_audit_presentation_utils.py +0 -0
  27. {awslabs_cloudwatch_appsignals_mcp_server-0.1.8 → awslabs_cloudwatch_appsignals_mcp_server-0.1.10}/tests/test_audit_utils.py +0 -0
  28. {awslabs_cloudwatch_appsignals_mcp_server-0.1.8 → awslabs_cloudwatch_appsignals_mcp_server-0.1.10}/tests/test_aws_profile.py +0 -0
  29. {awslabs_cloudwatch_appsignals_mcp_server-0.1.8 → awslabs_cloudwatch_appsignals_mcp_server-0.1.10}/tests/test_initialization.py +0 -0
  30. {awslabs_cloudwatch_appsignals_mcp_server-0.1.8 → awslabs_cloudwatch_appsignals_mcp_server-0.1.10}/tests/test_server_audit_functions.py +0 -0
  31. {awslabs_cloudwatch_appsignals_mcp_server-0.1.8 → awslabs_cloudwatch_appsignals_mcp_server-0.1.10}/tests/test_server_audit_tools.py +0 -0
  32. {awslabs_cloudwatch_appsignals_mcp_server-0.1.8 → awslabs_cloudwatch_appsignals_mcp_server-0.1.10}/tests/test_service_audit_utils.py +0 -0
  33. {awslabs_cloudwatch_appsignals_mcp_server-0.1.8 → awslabs_cloudwatch_appsignals_mcp_server-0.1.10}/tests/test_service_tools_operations.py +0 -0
  34. {awslabs_cloudwatch_appsignals_mcp_server-0.1.8 → awslabs_cloudwatch_appsignals_mcp_server-0.1.10}/tests/test_sli_report_client.py +0 -0
  35. {awslabs_cloudwatch_appsignals_mcp_server-0.1.8 → awslabs_cloudwatch_appsignals_mcp_server-0.1.10}/tests/test_slo_tools.py +0 -0
  36. {awslabs_cloudwatch_appsignals_mcp_server-0.1.8 → awslabs_cloudwatch_appsignals_mcp_server-0.1.10}/tests/test_utils.py +0 -0
  37. {awslabs_cloudwatch_appsignals_mcp_server-0.1.8 → awslabs_cloudwatch_appsignals_mcp_server-0.1.10}/uv-requirements.txt +0 -0
@@ -13,7 +13,7 @@
13
13
  # limitations under the License.
14
14
 
15
15
  # dependabot should continue to update this to the latest hash.
16
- FROM public.ecr.aws/docker/library/python:3.13.5-alpine3.21@sha256:c9a09c45a4bcc618c7f7128585b8dd0d41d0c31a8a107db4c8255ffe0b69375d AS uv
16
+ FROM public.ecr.aws/docker/library/python:3.13-alpine@sha256:070342a0cc1011532c0e69972cce2bbc6cc633eba294bae1d12abea8bd05303b AS uv
17
17
 
18
18
  # Install the project into `/app`
19
19
  WORKDIR /app
@@ -61,7 +61,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
61
61
  # Make the directory just in case it doesn't exist
62
62
  RUN mkdir -p /root/.local
63
63
 
64
- FROM public.ecr.aws/docker/library/python:3.13.5-alpine3.21@sha256:c9a09c45a4bcc618c7f7128585b8dd0d41d0c31a8a107db4c8255ffe0b69375d
64
+ FROM public.ecr.aws/docker/library/python:3.13-alpine@sha256:070342a0cc1011532c0e69972cce2bbc6cc633eba294bae1d12abea8bd05303b
65
65
 
66
66
  # Place executables in the environment at the front of the path and include other binaries
67
67
  ENV PATH="/app/.venv/bin:$PATH" \
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: awslabs.cloudwatch-appsignals-mcp-server
3
- Version: 0.1.8
3
+ Version: 0.1.10
4
4
  Summary: An AWS Labs Model Context Protocol (MCP) server for AWS Application Signals
5
5
  Project-URL: Homepage, https://awslabs.github.io/mcp/
6
6
  Project-URL: Documentation, https://awslabs.github.io/mcp/servers/cloudwatch-appsignals-mcp-server/
@@ -21,7 +21,7 @@ Classifier: Programming Language :: Python :: 3.11
21
21
  Classifier: Programming Language :: Python :: 3.12
22
22
  Classifier: Programming Language :: Python :: 3.13
23
23
  Requires-Python: >=3.10
24
- Requires-Dist: boto3>=1.37.24
24
+ Requires-Dist: boto3>=1.40.41
25
25
  Requires-Dist: httpx>=0.24.0
26
26
  Requires-Dist: loguru>=0.7.3
27
27
  Requires-Dist: mcp[cli]>=1.11.0
@@ -14,4 +14,4 @@
14
14
 
15
15
  """AWS Application Signals MCP Server."""
16
16
 
17
- __version__ = '0.1.8'
17
+ __version__ = '0.1.10'
@@ -191,7 +191,7 @@ async def search_transaction_spans(
191
191
 
192
192
  try:
193
193
  # Use default log group if none provided
194
- if log_group_name is None:
194
+ if not log_group_name:
195
195
  log_group_name = 'aws/spans'
196
196
  logger.debug('Using default log group: aws/spans')
197
197
 
@@ -402,6 +402,26 @@ async def query_sampled_traces(
402
402
  return obj.isoformat()
403
403
  return obj
404
404
 
405
+ # Helper function to extract fault message from root causes for deduplication
406
+ def get_fault_message(trace_data):
407
+ """Extract fault message from a trace for deduplication.
408
+
409
+ Only checks FaultRootCauses (5xx server errors) since this is the primary
410
+ use case for root cause investigation. Traces without fault messages are
411
+ not deduplicated.
412
+ """
413
+ # Only check FaultRootCauses for deduplication
414
+ root_causes = trace_data.get('FaultRootCauses', [])
415
+ if root_causes:
416
+ for cause in root_causes:
417
+ services = cause.get('Services', [])
418
+ for service in services:
419
+ exceptions = service.get('Exceptions', [])
420
+ if exceptions and exceptions[0].get('Message'):
421
+ return exceptions[0].get('Message')
422
+ return None
423
+
424
+ # Build trace summaries (original format)
405
425
  trace_summaries = []
406
426
  for trace in traces:
407
427
  # Create a simplified trace data structure to reduce size
@@ -417,17 +437,11 @@ async def query_sampled_traces(
417
437
 
418
438
  # Only include root causes if they exist (to save space)
419
439
  if trace.get('ErrorRootCauses'):
420
- trace_data['ErrorRootCauses'] = trace.get('ErrorRootCauses', [])[
421
- :3
422
- ] # Limit to first 3
440
+ trace_data['ErrorRootCauses'] = trace.get('ErrorRootCauses', [])[:3]
423
441
  if trace.get('FaultRootCauses'):
424
- trace_data['FaultRootCauses'] = trace.get('FaultRootCauses', [])[
425
- :3
426
- ] # Limit to first 3
442
+ trace_data['FaultRootCauses'] = trace.get('FaultRootCauses', [])[:3]
427
443
  if trace.get('ResponseTimeRootCauses'):
428
- trace_data['ResponseTimeRootCauses'] = trace.get('ResponseTimeRootCauses', [])[
429
- :3
430
- ] # Limit to first 3
444
+ trace_data['ResponseTimeRootCauses'] = trace.get('ResponseTimeRootCauses', [])[:3]
431
445
 
432
446
  # Include limited annotations for key operations
433
447
  annotations = trace.get('Annotations', {})
@@ -447,15 +461,50 @@ async def query_sampled_traces(
447
461
  # Convert any datetime objects to ISO format strings
448
462
  for key, value in trace_data.items():
449
463
  trace_data[key] = convert_datetime(value)
464
+
450
465
  trace_summaries.append(trace_data)
451
466
 
467
+ # Deduplicate trace summaries by fault message
468
+ seen_faults = {}
469
+ deduped_trace_summaries = []
470
+
471
+ for trace_summary in trace_summaries:
472
+ # Check if this trace has an error
473
+ has_issues = (
474
+ trace_summary.get('HasError')
475
+ or trace_summary.get('HasFault')
476
+ or trace_summary.get('HasThrottle')
477
+ )
478
+
479
+ if not has_issues:
480
+ # Always include healthy traces
481
+ deduped_trace_summaries.append(trace_summary)
482
+ continue
483
+
484
+ # Extract fault message for deduplication (only checks FaultRootCauses)
485
+ fault_msg = get_fault_message(trace_summary)
486
+
487
+ if fault_msg and fault_msg in seen_faults:
488
+ # Skip this trace - we already have one with the same fault message
489
+ seen_faults[fault_msg]['count'] += 1
490
+ logger.debug(
491
+ f'Skipping duplicate trace {trace_summary.get("Id")} - fault message already seen: {fault_msg[:100]}...'
492
+ )
493
+ continue
494
+ else:
495
+ # First time seeing this fault (or no fault message) - include it
496
+ deduped_trace_summaries.append(trace_summary)
497
+ if fault_msg:
498
+ seen_faults[fault_msg] = {'count': 1}
499
+
452
500
  # Check transaction search status
453
501
  is_tx_search_enabled, tx_destination, tx_status = check_transaction_search_enabled(region)
454
502
 
503
+ # Build response with original format but deduplicated traces
455
504
  result_data = {
456
- 'TraceSummaries': trace_summaries,
457
- 'TraceCount': len(trace_summaries),
458
- 'Message': f'Retrieved {len(trace_summaries)} traces (limited to prevent size issues)',
505
+ 'TraceSummaries': deduped_trace_summaries,
506
+ 'TraceCount': len(deduped_trace_summaries),
507
+ 'Message': f'Retrieved {len(deduped_trace_summaries)} unique traces from {len(trace_summaries)} total (deduplicated by fault message)',
459
508
  'SamplingNote': "⚠️ This data is from X-Ray's 5% sampling. Results may not show all errors or issues.",
460
509
  'TransactionSearchStatus': {
461
510
  'enabled': is_tx_search_enabled,
@@ -467,9 +516,18 @@ async def query_sampled_traces(
467
516
  },
468
517
  }
469
518
 
519
+ # Add dedup stats if we actually deduped anything
520
+ if len(deduped_trace_summaries) < len(trace_summaries):
521
+ duplicates_removed = len(trace_summaries) - len(deduped_trace_summaries)
522
+ result_data['DeduplicationStats'] = {
523
+ 'OriginalTraceCount': len(trace_summaries),
524
+ 'DuplicatesRemoved': duplicates_removed,
525
+ 'UniqueFaultMessages': len(seen_faults),
526
+ }
527
+
470
528
  elapsed_time = timer() - start_time_perf
471
529
  logger.info(
472
- f'query_sampled_traces completed in {elapsed_time:.3f}s - retrieved {len(trace_summaries)} traces'
530
+ f'query_sampled_traces completed in {elapsed_time:.3f}s - retrieved {len(deduped_trace_summaries)} unique traces from {len(trace_summaries)} total'
473
531
  )
474
532
  return json.dumps(result_data, indent=2)
475
533
 
@@ -1,11 +1,11 @@
1
1
  [project]
2
2
  name = "awslabs.cloudwatch-appsignals-mcp-server"
3
- version = "0.1.8"
3
+ version = "0.1.10"
4
4
  description = "An AWS Labs Model Context Protocol (MCP) server for AWS Application Signals"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
7
7
  dependencies = [
8
- "boto3>=1.37.24",
8
+ "boto3>=1.40.41",
9
9
  "httpx>=0.24.0",
10
10
  "loguru>=0.7.3",
11
11
  "mcp[cli]>=1.11.0",
@@ -1076,7 +1076,7 @@ async def test_search_transaction_spans_empty_log_group(mock_aws_clients):
1076
1076
  }
1077
1077
 
1078
1078
  await search_transaction_spans(
1079
- log_group_name='', # Empty string will be used as-is
1079
+ log_group_name='', # Empty string should default to 'aws/spans'
1080
1080
  start_time='2024-01-01T00:00:00+00:00',
1081
1081
  end_time='2024-01-01T01:00:00+00:00',
1082
1082
  query_string='fields @timestamp',
@@ -1084,10 +1084,10 @@ async def test_search_transaction_spans_empty_log_group(mock_aws_clients):
1084
1084
  max_timeout=30,
1085
1085
  )
1086
1086
 
1087
- # Verify start_query was called with empty string (current behavior)
1087
+ # Verify start_query was called with default 'aws/spans'
1088
1088
  mock_aws_clients['logs_client'].start_query.assert_called()
1089
1089
  call_args = mock_aws_clients['logs_client'].start_query.call_args[1]
1090
- assert '' in call_args['logGroupNames']
1090
+ assert 'aws/spans' in call_args['logGroupNames']
1091
1091
 
1092
1092
 
1093
1093
  @pytest.mark.asyncio
@@ -1634,7 +1634,7 @@ async def test_query_sampled_traces_with_fault_causes(mock_aws_clients):
1634
1634
  'Duration': 100,
1635
1635
  'HasFault': True,
1636
1636
  'FaultRootCauses': [
1637
- {'Services': [{'Name': 'service1'}]},
1637
+ {'Services': [{'Name': 'service1', 'Exceptions': [{'Message': 'Test fault error'}]}]},
1638
1638
  {'Services': [{'Name': 'service2'}]},
1639
1639
  {'Services': [{'Name': 'service3'}]},
1640
1640
  {'Services': [{'Name': 'service4'}]}, # Should be limited to 3
@@ -1709,6 +1709,157 @@ async def test_query_sampled_traces_datetime_conversion(mock_aws_clients):
1709
1709
  assert 'EndTime' not in trace_summary
1710
1710
 
1711
1711
 
1712
+ @pytest.mark.asyncio
1713
+ async def test_query_sampled_traces_deduplication(mock_aws_clients):
1714
+ """Test query_sampled_traces deduplicates traces with same fault message.
1715
+
1716
+ Note: Only FaultRootCauses are deduplicated, not ErrorRootCauses.
1717
+ This is because the primary use case is investigating server faults (5xx errors),
1718
+ not client errors (4xx).
1719
+ """
1720
+ # Create 5 traces with the same fault message
1721
+ mock_traces = [
1722
+ {
1723
+ 'Id': f'trace{i}',
1724
+ 'Duration': 100 + i * 10,
1725
+ 'ResponseTime': 95 + i * 10,
1726
+ 'HasFault': True,
1727
+ 'FaultRootCauses': [
1728
+ {
1729
+ 'Services': [
1730
+ {
1731
+ 'Name': 'test-service',
1732
+ 'Exceptions': [{'Message': 'Database connection timeout'}],
1733
+ }
1734
+ ]
1735
+ }
1736
+ ],
1737
+ }
1738
+ for i in range(1, 6)
1739
+ ]
1740
+
1741
+ # Add 2 traces with ErrorRootCauses (these should NOT be deduplicated)
1742
+ mock_traces.extend(
1743
+ [
1744
+ {
1745
+ 'Id': 'trace6',
1746
+ 'Duration': 200,
1747
+ 'HasError': True,
1748
+ 'ErrorRootCauses': [
1749
+ {
1750
+ 'Services': [
1751
+ {
1752
+ 'Name': 'api-service',
1753
+ 'Exceptions': [{'Message': 'Invalid API key'}],
1754
+ }
1755
+ ]
1756
+ }
1757
+ ],
1758
+ },
1759
+ {
1760
+ 'Id': 'trace7',
1761
+ 'Duration': 210,
1762
+ 'HasError': True,
1763
+ 'ErrorRootCauses': [
1764
+ {
1765
+ 'Services': [
1766
+ {
1767
+ 'Name': 'api-service',
1768
+ 'Exceptions': [{'Message': 'Invalid API key'}],
1769
+ }
1770
+ ]
1771
+ }
1772
+ ],
1773
+ },
1774
+ ]
1775
+ )
1776
+
1777
+ # Add 2 healthy traces
1778
+ mock_traces.extend(
1779
+ [
1780
+ {
1781
+ 'Id': 'trace8',
1782
+ 'Duration': 50,
1783
+ 'ResponseTime': 45,
1784
+ 'HasError': False,
1785
+ 'HasFault': False,
1786
+ },
1787
+ {
1788
+ 'Id': 'trace9',
1789
+ 'Duration': 55,
1790
+ 'ResponseTime': 50,
1791
+ 'HasError': False,
1792
+ 'HasFault': False,
1793
+ },
1794
+ ]
1795
+ )
1796
+
1797
+ with patch(
1798
+ 'awslabs.cloudwatch_appsignals_mcp_server.trace_tools.get_trace_summaries_paginated'
1799
+ ) as mock_paginated:
1800
+ mock_paginated.return_value = mock_traces
1801
+
1802
+ result_json = await query_sampled_traces(
1803
+ start_time='2024-01-01T00:00:00Z', end_time='2024-01-01T01:00:00Z'
1804
+ )
1805
+
1806
+ result = json.loads(result_json)
1807
+
1808
+ # Verify deduplication worked - should only have 5 traces
1809
+ # 1 for database timeout fault (deduplicated from 5)
1810
+ # 2 for API key errors (NOT deduplicated - only faults are deduped)
1811
+ # 2 healthy traces (not deduplicated)
1812
+ assert result['TraceCount'] == 5
1813
+ assert len(result['TraceSummaries']) == 5
1814
+
1815
+ # Verify deduplication stats
1816
+ assert 'DeduplicationStats' in result
1817
+ assert result['DeduplicationStats']['OriginalTraceCount'] == 9
1818
+ assert result['DeduplicationStats']['DuplicatesRemoved'] == 4 # 9 - 5 = 4
1819
+ assert (
1820
+ result['DeduplicationStats']['UniqueFaultMessages'] == 1
1821
+ ) # Only counting FaultRootCauses
1822
+
1823
+ # Find the trace with fault
1824
+ db_trace = next(
1825
+ (
1826
+ t
1827
+ for t in result['TraceSummaries']
1828
+ if t.get('FaultRootCauses')
1829
+ and any(
1830
+ 'Database connection timeout' in str(s.get('Exceptions', []))
1831
+ for cause in t['FaultRootCauses']
1832
+ for s in cause.get('Services', [])
1833
+ )
1834
+ ),
1835
+ None,
1836
+ )
1837
+ assert db_trace is not None
1838
+ assert db_trace['HasFault'] is True
1839
+
1840
+ # Verify both error traces are present (not deduplicated)
1841
+ error_traces = [
1842
+ t
1843
+ for t in result['TraceSummaries']
1844
+ if t.get('ErrorRootCauses')
1845
+ and any(
1846
+ 'Invalid API key' in str(s.get('Exceptions', []))
1847
+ for cause in t['ErrorRootCauses']
1848
+ for s in cause.get('Services', [])
1849
+ )
1850
+ ]
1851
+ assert len(error_traces) == 2 # Both error traces should be kept
1852
+ assert all(t['HasError'] is True for t in error_traces)
1853
+
1854
+ # Verify healthy traces are included
1855
+ healthy_count = sum(
1856
+ 1
1857
+ for t in result['TraceSummaries']
1858
+ if not t.get('HasError') and not t.get('HasFault') and not t.get('HasThrottle')
1859
+ )
1860
+ assert healthy_count == 2
1861
+
1862
+
1712
1863
  def test_main_success(mock_aws_clients):
1713
1864
  """Test main function normal execution."""
1714
1865
  with patch('awslabs.cloudwatch_appsignals_mcp_server.server.mcp') as mock_mcp:
@@ -46,7 +46,7 @@ wheels = [
46
46
 
47
47
  [[package]]
48
48
  name = "awslabs-cloudwatch-appsignals-mcp-server"
49
- version = "0.1.8"
49
+ version = "0.1.10"
50
50
  source = { editable = "." }
51
51
  dependencies = [
52
52
  { name = "boto3" },
@@ -72,7 +72,7 @@ dev = [
72
72
 
73
73
  [package.metadata]
74
74
  requires-dist = [
75
- { name = "boto3", specifier = ">=1.37.24" },
75
+ { name = "boto3", specifier = ">=1.40.41" },
76
76
  { name = "httpx", specifier = ">=0.24.0" },
77
77
  { name = "loguru", specifier = ">=0.7.3" },
78
78
  { name = "mcp", extras = ["cli"], specifier = ">=1.11.0" },
@@ -110,16 +110,16 @@ wheels = [
110
110
 
111
111
  [[package]]
112
112
  name = "boto3"
113
- version = "1.37.24"
113
+ version = "1.40.41"
114
114
  source = { registry = "https://pypi.org/simple" }
115
115
  dependencies = [
116
116
  { name = "botocore" },
117
117
  { name = "jmespath" },
118
118
  { name = "s3transfer" },
119
119
  ]
120
- sdist = { url = "https://files.pythonhosted.org/packages/f9/1c/3901ff3ea6a9ddc7de17aade70b4ee2c25edd91f0a772bdb3419b58014a2/boto3-1.37.24.tar.gz", hash = "sha256:1d3c6fc63a9efba0af8b531ec6b7f7c6b0ef197bf3dcd875f03c9097ac68b58f", size = 111368, upload-time = "2025-03-31T19:35:17.771Z" }
120
+ sdist = { url = "https://files.pythonhosted.org/packages/52/f7/d652732fd8fc28f427f3e698e488b7422ede535fe4d813c7988642c7734b/boto3-1.40.41.tar.gz", hash = "sha256:2ea2463fc42812f3cab66b53114579b1f4b9a378ee48921d4385511a94307b24", size = 111621, upload-time = "2025-09-29T19:21:14.928Z" }
121
121
  wheels = [
122
- { url = "https://files.pythonhosted.org/packages/2d/fa/8ea42eff98e02962473f60c11663282cd8b8c04cc66eab954184325516ac/boto3-1.37.24-py3-none-any.whl", hash = "sha256:2f2b8f82a5d7f89283973bf2cab771b90c09348799e78b2a25c60cd22c443514", size = 139561, upload-time = "2025-03-31T19:35:14.96Z" },
122
+ { url = "https://files.pythonhosted.org/packages/45/a8/d5c924e3dfce8804ff7119c1e84f423414851a82d30d744e81f4b61ddbff/boto3-1.40.41-py3-none-any.whl", hash = "sha256:02eac942aaa9f3a1c8a11f77e6f971b41c125973888f80f3eb177c2f21ad7a01", size = 139342, upload-time = "2025-09-29T19:21:12.728Z" },
123
123
  ]
124
124
 
125
125
  [[package]]
@@ -152,16 +152,16 @@ xray = [
152
152
 
153
153
  [[package]]
154
154
  name = "botocore"
155
- version = "1.37.38"
155
+ version = "1.40.41"
156
156
  source = { registry = "https://pypi.org/simple" }
157
157
  dependencies = [
158
158
  { name = "jmespath" },
159
159
  { name = "python-dateutil" },
160
160
  { name = "urllib3" },
161
161
  ]
162
- sdist = { url = "https://files.pythonhosted.org/packages/34/79/4e072e614339727f79afef704e5993b5b4d2667c1671c757cc4deb954744/botocore-1.37.38.tar.gz", hash = "sha256:c3ea386177171f2259b284db6afc971c959ec103fa2115911c4368bea7cbbc5d", size = 13832365, upload-time = "2025-04-21T19:27:05.245Z" }
162
+ sdist = { url = "https://files.pythonhosted.org/packages/c8/55/fa23104e4352f28a457c452a3621b00fe2a7a1e096d04e2f4a9890250448/botocore-1.40.41.tar.gz", hash = "sha256:320873c6a34bfd64fb9bbc55e8ac38e7904a574cfc634d1f0f66b1490c62b89d", size = 14365601, upload-time = "2025-09-29T19:21:03.667Z" }
163
163
  wheels = [
164
- { url = "https://files.pythonhosted.org/packages/55/1b/93f3504afc7c523dcaa8a8147cfc75421983e30b08d9f93a533929589630/botocore-1.37.38-py3-none-any.whl", hash = "sha256:23b4097780e156a4dcaadfc1ed156ce25cb95b6087d010c4bb7f7f5d9bc9d219", size = 13499391, upload-time = "2025-04-21T19:27:00.869Z" },
164
+ { url = "https://files.pythonhosted.org/packages/c3/d2/1b5bc59c746413d3eca16bd659b8c00cf2d4eeb6274053d1ff3110d13670/botocore-1.40.41-py3-none-any.whl", hash = "sha256:8246bf73a2e20647cf1d4dae1e9a7c40f97f38a34a6a1fbfd49aa6b3dce5ffaa", size = 14034516, upload-time = "2025-09-29T19:20:59.792Z" },
165
165
  ]
166
166
 
167
167
  [[package]]
@@ -1405,14 +1405,14 @@ wheels = [
1405
1405
 
1406
1406
  [[package]]
1407
1407
  name = "s3transfer"
1408
- version = "0.11.5"
1408
+ version = "0.14.0"
1409
1409
  source = { registry = "https://pypi.org/simple" }
1410
1410
  dependencies = [
1411
1411
  { name = "botocore" },
1412
1412
  ]
1413
- sdist = { url = "https://files.pythonhosted.org/packages/c4/2b/5c9562795c2eb2b5f63536961754760c25bf0f34af93d36aa28dea2fb303/s3transfer-0.11.5.tar.gz", hash = "sha256:8c8aad92784779ab8688a61aefff3e28e9ebdce43142808eaa3f0b0f402f68b7", size = 149107, upload-time = "2025-04-17T19:23:19.051Z" }
1413
+ sdist = { url = "https://files.pythonhosted.org/packages/62/74/8d69dcb7a9efe8baa2046891735e5dfe433ad558ae23d9e3c14c633d1d58/s3transfer-0.14.0.tar.gz", hash = "sha256:eff12264e7c8b4985074ccce27a3b38a485bb7f7422cc8046fee9be4983e4125", size = 151547, upload-time = "2025-09-09T19:23:31.089Z" }
1414
1414
  wheels = [
1415
- { url = "https://files.pythonhosted.org/packages/45/39/13402e323666d17850eca87e4cd6ecfcf9fd7809cac9efdcce10272fc29d/s3transfer-0.11.5-py3-none-any.whl", hash = "sha256:757af0f2ac150d3c75bc4177a32355c3862a98d20447b69a0161812992fe0bd4", size = 84782, upload-time = "2025-04-17T19:23:17.516Z" },
1415
+ { url = "https://files.pythonhosted.org/packages/48/f0/ae7ca09223a81a1d890b2557186ea015f6e0502e9b8cb8e1813f1d8cfa4e/s3transfer-0.14.0-py3-none-any.whl", hash = "sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456", size = 85712, upload-time = "2025-09-09T19:23:30.041Z" },
1416
1416
  ]
1417
1417
 
1418
1418
  [[package]]