This is page 500 of 503. Use http://codebase.md/awslabs/mcp?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .devcontainer
│ └── devcontainer.json
├── .github
│ ├── actions
│ │ ├── build-and-push-container-image
│ │ │ └── action.yml
│ │ └── clear-space-ubuntu-latest-agressively
│ │ └── action.yml
│ ├── codecov.yml
│ ├── CODEOWNERS
│ ├── dependabot.yml
│ ├── ISSUE_TEMPLATE
│ │ ├── bug_report.yml
│ │ ├── documentation.yml
│ │ ├── feature_request.yml
│ │ ├── rfc.yml
│ │ └── support_awslabs_mcp_servers.yml
│ ├── pull_request_template.md
│ ├── SECURITY
│ ├── SUPPORT
│ └── workflows
│ ├── aws-api-mcp-upgrade-version.yml
│ ├── bandit-requirements.txt
│ ├── bandit.yml
│ ├── cfn_nag.yml
│ ├── check-gh-pages-builds.yml
│ ├── check-license-header-hash.txt
│ ├── check-license-header.json
│ ├── check-license-header.yml
│ ├── checkov.yml
│ ├── codeql.yml
│ ├── dependency-review-action.yml
│ ├── detect-secrets-requirements.txt
│ ├── gh-pages.yml
│ ├── merge-prevention.yml
│ ├── powershell.yml
│ ├── pre-commit-requirements.txt
│ ├── pre-commit.yml
│ ├── pull-request-lint.yml
│ ├── python.yml
│ ├── RELEASE_INSTRUCTIONS.md
│ ├── release-initiate-branch.yml
│ ├── release-merge-tag.yml
│ ├── release.py
│ ├── release.yml
│ ├── scanners.yml
│ ├── scorecard-analysis.yml
│ ├── semgrep-requirements.txt
│ ├── semgrep.yml
│ ├── stale.yml
│ ├── trivy.yml
│ └── typescript.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .python-version
├── .ruff.toml
├── .secrets.baseline
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── DESIGN_GUIDELINES.md
├── DEVELOPER_GUIDE.md
├── docs
│ └── images
│ └── root-readme
│ ├── cline-api-provider-filled.png
│ ├── cline-chat-interface.png
│ ├── cline-custom-instructions.png
│ ├── cline-select-aws-profile.png
│ ├── cline-select-bedrock.png
│ ├── configure-mcp-servers.png
│ ├── install-cline-extension.png
│ ├── mcp-servers-installed.png
│ └── select-mcp-servers.png
├── docusaurus
│ ├── .gitignore
│ ├── docs
│ │ ├── installation.md
│ │ ├── intro.md
│ │ ├── samples
│ │ │ ├── index.md
│ │ │ ├── mcp-integration-with-kb.md
│ │ │ ├── mcp-integration-with-nova-canvas.md
│ │ │ └── stepfunctions-tool-mcp-server.md
│ │ ├── servers
│ │ │ ├── amazon-bedrock-agentcore-mcp-server.md
│ │ │ ├── amazon-keyspaces-mcp-server.md
│ │ │ ├── amazon-mq-mcp-server.md
│ │ │ ├── amazon-neptune-mcp-server.md
│ │ │ ├── amazon-qbusiness-anonymous-mcp-server.md
│ │ │ ├── amazon-qindex-mcp-server.md
│ │ │ ├── amazon-sns-sqs-mcp-server.md
│ │ │ ├── aurora-dsql-mcp-server.md
│ │ │ ├── aws-api-mcp-server.md
│ │ │ ├── aws-appsync-mcp-server.md
│ │ │ ├── aws-bedrock-custom-model-import-mcp-server.md
│ │ │ ├── aws-bedrock-data-automation-mcp-server.md
│ │ │ ├── aws-dataprocessing-mcp-server.md
│ │ │ ├── aws-diagram-mcp-server.md
│ │ │ ├── aws-documentation-mcp-server.md
│ │ │ ├── aws-healthomics-mcp-server.md
│ │ │ ├── aws-iot-sitewise-mcp-server.md
│ │ │ ├── aws-knowledge-mcp-server.md
│ │ │ ├── aws-location-mcp-server.md
│ │ │ ├── aws-msk-mcp-server.md
│ │ │ ├── aws-pricing-mcp-server.md
│ │ │ ├── aws-serverless-mcp-server.md
│ │ │ ├── aws-support-mcp-server.md
│ │ │ ├── bedrock-kb-retrieval-mcp-server.md
│ │ │ ├── billing-cost-management-mcp-server.md
│ │ │ ├── ccapi-mcp-server.md
│ │ │ ├── cdk-mcp-server.md
│ │ │ ├── cfn-mcp-server.md
│ │ │ ├── cloudtrail-mcp-server.md
│ │ │ ├── cloudwatch-appsignals-mcp-server.md
│ │ │ ├── cloudwatch-mcp-server.md
│ │ │ ├── code-doc-gen-mcp-server.md
│ │ │ ├── core-mcp-server.md
│ │ │ ├── cost-explorer-mcp-server.md
│ │ │ ├── documentdb-mcp-server.md
│ │ │ ├── dynamodb-mcp-server.md
│ │ │ ├── ecs-mcp-server.md
│ │ │ ├── eks-mcp-server.md
│ │ │ ├── elasticache-mcp-server.md
│ │ │ ├── finch-mcp-server.md
│ │ │ ├── frontend-mcp-server.md
│ │ │ ├── git-repo-research-mcp-server.md
│ │ │ ├── healthlake-mcp-server.md
│ │ │ ├── iam-mcp-server.md
│ │ │ ├── kendra-index-mcp-server.md
│ │ │ ├── lambda-tool-mcp-server.md
│ │ │ ├── memcached-mcp-server.md
│ │ │ ├── mysql-mcp-server.md
│ │ │ ├── nova-canvas-mcp-server.md
│ │ │ ├── openapi-mcp-server.md
│ │ │ ├── postgres-mcp-server.md
│ │ │ ├── prometheus-mcp-server.md
│ │ │ ├── redshift-mcp-server.md
│ │ │ ├── s3-tables-mcp-server.md
│ │ │ ├── stepfunctions-tool-mcp-server.md
│ │ │ ├── syntheticdata-mcp-server.md
│ │ │ ├── terraform-mcp-server.md
│ │ │ ├── timestream-for-influxdb-mcp-server.md
│ │ │ ├── valkey-mcp-server.md
│ │ │ └── well-architected-security-mcp-server.mdx
│ │ └── vibe_coding.md
│ ├── docusaurus.config.ts
│ ├── package-lock.json
│ ├── package.json
│ ├── README.md
│ ├── sidebars.ts
│ ├── src
│ │ ├── components
│ │ │ ├── HomepageFeatures
│ │ │ │ └── styles.module.css
│ │ │ └── ServerCards
│ │ │ ├── index.tsx
│ │ │ └── styles.module.css
│ │ ├── css
│ │ │ ├── custom.css
│ │ │ └── doc-override.css
│ │ └── pages
│ │ ├── index.module.css
│ │ └── servers.tsx
│ ├── static
│ │ ├── .nojekyll
│ │ ├── assets
│ │ │ ├── icons
│ │ │ │ ├── activity.svg
│ │ │ │ ├── book-open.svg
│ │ │ │ ├── cpu.svg
│ │ │ │ ├── database.svg
│ │ │ │ ├── dollar-sign.svg
│ │ │ │ ├── help-circle.svg
│ │ │ │ ├── key.svg
│ │ │ │ ├── server.svg
│ │ │ │ ├── share-2.svg
│ │ │ │ ├── tool.svg
│ │ │ │ └── zap.svg
│ │ │ └── server-cards.json
│ │ └── img
│ │ ├── aws-logo.svg
│ │ └── logo.png
│ └── tsconfig.json
├── LICENSE
├── NOTICE
├── README.md
├── samples
│ ├── mcp-integration-with-kb
│ │ ├── .env.example
│ │ ├── .python-version
│ │ ├── assets
│ │ │ └── simplified-mcp-flow-diagram.png
│ │ ├── clients
│ │ │ └── client_server.py
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── user_interfaces
│ │ │ └── chat_bedrock_st.py
│ │ └── uv.lock
│ ├── mcp-integration-with-nova-canvas
│ │ ├── .env.example
│ │ ├── .python-version
│ │ ├── clients
│ │ │ └── client_server.py
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── user_interfaces
│ │ │ └── image_generator_st.py
│ │ └── uv.lock
│ ├── README.md
│ └── stepfunctions-tool-mcp-server
│ ├── README.md
│ └── sample_state_machines
│ ├── customer-create
│ │ └── app.py
│ ├── customer-id-from-email
│ │ └── app.py
│ ├── customer-info-from-id
│ │ └── app.py
│ └── template.yml
├── src
│ ├── amazon-bedrock-agentcore-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── amazon_bedrock_agentcore_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── config.py
│ │ │ ├── server.py
│ │ │ └── utils
│ │ │ ├── __init__.py
│ │ │ ├── cache.py
│ │ │ ├── doc_fetcher.py
│ │ │ ├── indexer.py
│ │ │ ├── text_processor.py
│ │ │ └── url_validator.py
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── SECURITY.md
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── conftest.py
│ │ │ ├── test_cache.py
│ │ │ ├── test_config.py
│ │ │ ├── test_doc_fetcher.py
│ │ │ ├── test_indexer.py
│ │ │ ├── test_init.py
│ │ │ ├── test_main.py
│ │ │ ├── test_server.py
│ │ │ ├── test_text_processor.py
│ │ │ └── test_url_validator.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── amazon-kendra-index-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── amazon_kendra_index_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── server.py
│ │ │ └── util.py
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── test_init.py
│ │ │ ├── test_main.py
│ │ │ └── test_server.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── amazon-keyspaces-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── amazon_keyspaces_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── client.py
│ │ │ ├── config.py
│ │ │ ├── consts.py
│ │ │ ├── llm_context.py
│ │ │ ├── models.py
│ │ │ ├── server.py
│ │ │ └── services.py
│ │ ├── CHANGELOG.md
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── run_tests.sh
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── test_client.py
│ │ │ ├── test_init.py
│ │ │ ├── test_main.py
│ │ │ ├── test_query_analysis_service.py
│ │ │ ├── test_server.py
│ │ │ └── test_services.py
│ │ └── uv.lock
│ ├── amazon-mq-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── amazon_mq_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── aws_service_mcp_generator.py
│ │ │ ├── consts.py
│ │ │ ├── rabbitmq
│ │ │ │ ├── __init__.py
│ │ │ │ ├── admin.py
│ │ │ │ ├── connection.py
│ │ │ │ ├── doc
│ │ │ │ │ ├── rabbitmq_broker_sizing_guide.md
│ │ │ │ │ ├── rabbitmq_performance_optimization_best_practice.md
│ │ │ │ │ ├── rabbitmq_production_deployment_guidelines.md
│ │ │ │ │ ├── rabbitmq_quorum_queue_migration_guide.md
│ │ │ │ │ └── rabbitmq_setup_best_practice.md
│ │ │ │ ├── handlers.py
│ │ │ │ └── module.py
│ │ │ └── server.py
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── example
│ │ │ └── sample_mcp_q_cli.json
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── .gitignore
│ │ │ ├── rabbitmq
│ │ │ │ ├── __init__.py
│ │ │ │ ├── conftest.py
│ │ │ │ ├── test_admin.py
│ │ │ │ ├── test_connection.py
│ │ │ │ ├── test_handlers.py
│ │ │ │ └── test_module.py
│ │ │ ├── test_aws_service_mcp_generator.py
│ │ │ └── test_server.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── amazon-neptune-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── amazon_neptune_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── exceptions.py
│ │ │ ├── graph_store
│ │ │ │ ├── __init__.py
│ │ │ │ ├── analytics.py
│ │ │ │ ├── base.py
│ │ │ │ └── database.py
│ │ │ ├── models.py
│ │ │ ├── neptune.py
│ │ │ └── server.py
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── conftest.py
│ │ │ ├── test_analytics.py
│ │ │ ├── test_database.py
│ │ │ ├── test_exceptions.py
│ │ │ ├── test_init.py
│ │ │ ├── test_main.py
│ │ │ ├── test_models.py
│ │ │ ├── test_neptune.py
│ │ │ └── test_server.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── amazon-qbusiness-anonymous-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── amazon_qbusiness_anonymous_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── clients.py
│ │ │ └── server.py
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── conftest.py
│ │ │ ├── test_init.py
│ │ │ ├── test_main.py
│ │ │ └── test_server.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── amazon-qindex-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── amazon_qindex_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── clients.py
│ │ │ └── server.py
│ │ ├── CHANGELOG.md
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── test_clients.py
│ │ │ ├── test_init.py
│ │ │ ├── test_main.py
│ │ │ └── test_server.py
│ │ └── uv.lock
│ ├── amazon-sns-sqs-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── amazon_sns_sqs_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── common.py
│ │ │ ├── consts.py
│ │ │ ├── generator.py
│ │ │ ├── server.py
│ │ │ ├── sns.py
│ │ │ └── sqs.py
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── print_tools.py
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── run_tests.sh
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── .gitignore
│ │ │ ├── README.md
│ │ │ ├── test_common.py
│ │ │ ├── test_generator.py
│ │ │ ├── test_server.py
│ │ │ ├── test_sns.py
│ │ │ └── test_sqs.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── aurora-dsql-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── aurora_dsql_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── consts.py
│ │ │ ├── mutable_sql_detector.py
│ │ │ └── server.py
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── test_connection_reuse.py
│ │ │ ├── test_init.py
│ │ │ ├── test_main.py
│ │ │ ├── test_profile_option.py
│ │ │ ├── test_readonly_enforcement.py
│ │ │ └── test_server.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── aws-api-mcp-server
│ │ ├── .gitattributes
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── aws_api_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── core
│ │ │ │ ├── __init__.py
│ │ │ │ ├── agent_scripts
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── manager.py
│ │ │ │ │ ├── models.py
│ │ │ │ │ └── registry
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── application-failure-troubleshooting.script.md
│ │ │ │ │ ├── cloudtral-mutli-region-setup.script.md
│ │ │ │ │ ├── create_amazon_aurora_db_cluster_with_instances.script.md
│ │ │ │ │ ├── lambda-timeout-debugging.script.md
│ │ │ │ │ ├── scripts_format.md
│ │ │ │ │ └── troubleshoot-permissions-with-cloudtrail-events.script.md
│ │ │ │ ├── aws
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── driver.py
│ │ │ │ │ ├── pagination.py
│ │ │ │ │ ├── regions.py
│ │ │ │ │ ├── service.py
│ │ │ │ │ └── services.py
│ │ │ │ ├── common
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── command_metadata.py
│ │ │ │ │ ├── command.py
│ │ │ │ │ ├── config.py
│ │ │ │ │ ├── errors.py
│ │ │ │ │ ├── file_operations.py
│ │ │ │ │ ├── file_system_controls.py
│ │ │ │ │ ├── helpers.py
│ │ │ │ │ ├── models.py
│ │ │ │ │ └── py.typed
│ │ │ │ ├── data
│ │ │ │ │ └── api_metadata.json
│ │ │ │ ├── metadata
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ └── read_only_operations_list.py
│ │ │ │ ├── parser
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── custom_validators
│ │ │ │ │ │ ├── __init__.py
│ │ │ │ │ │ ├── botocore_param_validator.py
│ │ │ │ │ │ ├── ec2_validator.py
│ │ │ │ │ │ └── ssm_validator.py
│ │ │ │ │ ├── interpretation.py
│ │ │ │ │ ├── lexer.py
│ │ │ │ │ └── parser.py
│ │ │ │ ├── py.typed
│ │ │ │ └── security
│ │ │ │ ├── __init__.py
│ │ │ │ ├── aws_api_customization.json
│ │ │ │ └── policy.py
│ │ │ └── server.py
│ │ ├── CHANGELOG.md
│ │ ├── CONTRIBUTING.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── agent_scripts
│ │ │ │ ├── __init__.py
│ │ │ │ ├── test_manager.py
│ │ │ │ └── test_registry
│ │ │ │ ├── another_valid_script.script.md
│ │ │ │ ├── test_script.script.md
│ │ │ │ └── valid_script.script.md
│ │ │ ├── aws
│ │ │ │ ├── __init__.py
│ │ │ │ ├── test_driver.py
│ │ │ │ ├── test_pagination.py
│ │ │ │ ├── test_service.py
│ │ │ │ └── test_services.py
│ │ │ ├── common
│ │ │ │ ├── test_command.py
│ │ │ │ ├── test_config.py
│ │ │ │ ├── test_file_operations.py
│ │ │ │ ├── test_file_system_controls.py
│ │ │ │ ├── test_file_validation.py
│ │ │ │ └── test_helpers.py
│ │ │ ├── fixtures.py
│ │ │ ├── history_handler.py
│ │ │ ├── metadata
│ │ │ │ ├── __init__.py
│ │ │ │ └── test_read_only_operations_list.py
│ │ │ ├── parser
│ │ │ │ ├── __init__.py
│ │ │ │ ├── test_file_path_detection.py
│ │ │ │ ├── test_lexer.py
│ │ │ │ ├── test_parser_customizations.py
│ │ │ │ └── test_parser.py
│ │ │ ├── test_security_policy.py
│ │ │ └── test_server.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── aws-appsync-mcp-server
│ │ ├── .dockerignore
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── aws_appsync_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── decorators.py
│ │ │ ├── helpers.py
│ │ │ ├── operations
│ │ │ │ ├── __init__.py
│ │ │ │ ├── create_api_cache.py
│ │ │ │ ├── create_api_key.py
│ │ │ │ ├── create_api.py
│ │ │ │ ├── create_channel_namespace.py
│ │ │ │ ├── create_datasource.py
│ │ │ │ ├── create_domain_name.py
│ │ │ │ ├── create_function.py
│ │ │ │ ├── create_graphql_api.py
│ │ │ │ ├── create_resolver.py
│ │ │ │ └── create_schema.py
│ │ │ ├── server.py
│ │ │ ├── tools
│ │ │ │ ├── __init__.py
│ │ │ │ ├── create_api_cache.py
│ │ │ │ ├── create_api_key.py
│ │ │ │ ├── create_api.py
│ │ │ │ ├── create_channel_namespace.py
│ │ │ │ ├── create_datasource.py
│ │ │ │ ├── create_domain_name.py
│ │ │ │ ├── create_function.py
│ │ │ │ ├── create_graphql_api.py
│ │ │ │ ├── create_resolver.py
│ │ │ │ └── create_schema.py
│ │ │ └── validators.py
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── test_all_create_tools_write_protection.py
│ │ │ ├── test_create_api_cache.py
│ │ │ ├── test_create_api_key.py
│ │ │ ├── test_create_api.py
│ │ │ ├── test_create_channel_namespace.py
│ │ │ ├── test_create_datasource_tool.py
│ │ │ ├── test_create_datasource.py
│ │ │ ├── test_create_domain_name.py
│ │ │ ├── test_create_function.py
│ │ │ ├── test_create_graphql_api.py
│ │ │ ├── test_create_resolver.py
│ │ │ ├── test_create_schema_tool.py
│ │ │ ├── test_create_schema.py
│ │ │ ├── test_helpers.py
│ │ │ ├── test_server.py
│ │ │ ├── test_validators.py
│ │ │ └── test_write_operation.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── aws-bedrock-custom-model-import-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── aws_bedrock_custom_model_import_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── client.py
│ │ │ ├── llm_context.py
│ │ │ ├── models.py
│ │ │ ├── prompts.py
│ │ │ ├── server.py
│ │ │ ├── services
│ │ │ │ ├── __init__.py
│ │ │ │ ├── imported_model_service.py
│ │ │ │ └── model_import_service.py
│ │ │ ├── tools
│ │ │ │ ├── create_model_import_job.py
│ │ │ │ ├── delete_imported_model.py
│ │ │ │ ├── get_imported_model.py
│ │ │ │ ├── get_model_import_job.py
│ │ │ │ ├── list_imported_models.py
│ │ │ │ └── list_model_import_jobs.py
│ │ │ └── utils
│ │ │ ├── __init__.py
│ │ │ ├── aws.py
│ │ │ ├── config.py
│ │ │ ├── consts.py
│ │ │ └── matching.py
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── services
│ │ │ │ ├── test_imported_model_service.py
│ │ │ │ └── test_model_import_service.py
│ │ │ ├── test_client.py
│ │ │ ├── test_init.py
│ │ │ ├── test_llm_context.py
│ │ │ ├── test_prompts.py
│ │ │ ├── test_server.py
│ │ │ ├── tools
│ │ │ │ ├── test_create_model_import_job.py
│ │ │ │ ├── test_delete_imported_model.py
│ │ │ │ ├── test_get_imported_model.py
│ │ │ │ ├── test_get_model_import_job.py
│ │ │ │ ├── test_list_imported_models.py
│ │ │ │ └── test_list_model_import_jobs.py
│ │ │ └── utils
│ │ │ ├── test_aws.py
│ │ │ ├── test_config.py
│ │ │ └── test_matching.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── aws-bedrock-data-automation-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── aws_bedrock_data_automation_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── helpers.py
│ │ │ └── server.py
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── test_helpers.py
│ │ │ ├── test_init.py
│ │ │ ├── test_main.py
│ │ │ └── test_server.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── aws-dataprocessing-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── aws_dataprocessing_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── core
│ │ │ │ ├── __init__.py
│ │ │ │ └── glue_data_catalog
│ │ │ │ ├── __init__.py
│ │ │ │ ├── data_catalog_database_manager.py
│ │ │ │ ├── data_catalog_handler.py
│ │ │ │ └── data_catalog_table_manager.py
│ │ │ ├── handlers
│ │ │ │ ├── __init__.py
│ │ │ │ ├── athena
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── athena_data_catalog_handler.py
│ │ │ │ │ ├── athena_query_handler.py
│ │ │ │ │ └── athena_workgroup_handler.py
│ │ │ │ ├── commons
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ └── common_resource_handler.py
│ │ │ │ ├── emr
│ │ │ │ │ ├── emr_ec2_cluster_handler.py
│ │ │ │ │ ├── emr_ec2_instance_handler.py
│ │ │ │ │ └── emr_ec2_steps_handler.py
│ │ │ │ └── glue
│ │ │ │ ├── __init__.py
│ │ │ │ ├── crawler_handler.py
│ │ │ │ ├── data_catalog_handler.py
│ │ │ │ ├── glue_commons_handler.py
│ │ │ │ ├── glue_etl_handler.py
│ │ │ │ ├── interactive_sessions_handler.py
│ │ │ │ └── worklows_handler.py
│ │ │ ├── models
│ │ │ │ ├── __init__.py
│ │ │ │ ├── athena_models.py
│ │ │ │ ├── common_resource_models.py
│ │ │ │ ├── data_catalog_models.py
│ │ │ │ ├── emr_models.py
│ │ │ │ └── glue_models.py
│ │ │ ├── server.py
│ │ │ └── utils
│ │ │ ├── __init__.py
│ │ │ ├── aws_helper.py
│ │ │ ├── consts.py
│ │ │ ├── logging_helper.py
│ │ │ └── mutable_sql_detector.py
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── core
│ │ │ │ ├── __init__.py
│ │ │ │ └── glue_data_catalog
│ │ │ │ ├── __init__.py
│ │ │ │ ├── test_data_catalog_database_manager.py
│ │ │ │ ├── test_data_catalog_handler.py
│ │ │ │ └── test_data_catalog_table_manager.py
│ │ │ ├── handlers
│ │ │ │ ├── __init__.py
│ │ │ │ ├── athena
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── test_athena_data_catalog_handler.py
│ │ │ │ │ ├── test_athena_query_handler.py
│ │ │ │ │ ├── test_athena_workgroup_handler.py
│ │ │ │ │ └── test_custom_tags_athena.py
│ │ │ │ ├── commons
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ └── test_common_resource_handler.py
│ │ │ │ ├── emr
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── test_custom_tags_emr.py
│ │ │ │ │ ├── test_emr_ec2_cluster_handler.py
│ │ │ │ │ ├── test_emr_ec2_instance_handler.py
│ │ │ │ │ └── test_emr_ec2_steps_handler.py
│ │ │ │ └── glue
│ │ │ │ ├── __init__.py
│ │ │ │ ├── test_crawler_handler.py
│ │ │ │ ├── test_custom_tags_glue.py
│ │ │ │ ├── test_data_catalog_handler.py
│ │ │ │ ├── test_glue_commons_handler.py
│ │ │ │ ├── test_glue_etl_handler.py
│ │ │ │ ├── test_glue_interactive_sessions_handler.py
│ │ │ │ └── test_glue_workflows_handler.py
│ │ │ ├── models
│ │ │ │ ├── __init__.py
│ │ │ │ ├── test_athena_models.py
│ │ │ │ ├── test_common_resource_models.py
│ │ │ │ ├── test_data_catalog_models.py
│ │ │ │ ├── test_emr_models.py
│ │ │ │ ├── test_glue_models.py
│ │ │ │ ├── test_interactive_sessions_models.py
│ │ │ │ └── test_workflows_models.py
│ │ │ ├── test_init.py
│ │ │ ├── test_server.py
│ │ │ └── utils
│ │ │ ├── __init__.py
│ │ │ ├── test_aws_helper.py
│ │ │ ├── test_custom_tags.py
│ │ │ └── test_logging_helper.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── aws-diagram-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── aws_diagram_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── diagrams_tools.py
│ │ │ ├── models.py
│ │ │ ├── scanner.py
│ │ │ └── server.py
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── .gitignore
│ │ │ ├── conftest.py
│ │ │ ├── README.md
│ │ │ ├── resources
│ │ │ │ ├── __init__.py
│ │ │ │ └── example_diagrams
│ │ │ │ ├── __init__.py
│ │ │ │ ├── aws_example.py
│ │ │ │ ├── flow_example.py
│ │ │ │ └── sequence_example.py
│ │ │ ├── test_diagrams.py
│ │ │ ├── test_models.py
│ │ │ ├── test_sarif_fix.py
│ │ │ ├── test_scanner.py
│ │ │ └── test_server.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── aws-documentation-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── aws_documentation_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── models.py
│ │ │ ├── server_aws_cn.py
│ │ │ ├── server_aws.py
│ │ │ ├── server_utils.py
│ │ │ ├── server.py
│ │ │ └── util.py
│ │ ├── basic-usage.gif
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── conftest.py
│ │ │ ├── constants.py
│ │ │ ├── resources
│ │ │ │ └── lambda_sns_raw.html
│ │ │ ├── test_aws_cn_get_available_services_live.py
│ │ │ ├── test_aws_cn_read_documentation_live.py
│ │ │ ├── test_aws_read_documentation_live.py
│ │ │ ├── test_aws_recommend_live.py
│ │ │ ├── test_aws_search_live.py
│ │ │ ├── test_metadata_handling.py
│ │ │ ├── test_models.py
│ │ │ ├── test_server_aws_cn.py
│ │ │ ├── test_server_aws.py
│ │ │ ├── test_server_utils.py
│ │ │ ├── test_server.py
│ │ │ └── test_util.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── aws-healthomics-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── aws_healthomics_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── consts.py
│ │ │ ├── models.py
│ │ │ ├── server.py
│ │ │ ├── tools
│ │ │ │ ├── __init__.py
│ │ │ │ ├── helper_tools.py
│ │ │ │ ├── run_analysis.py
│ │ │ │ ├── troubleshooting.py
│ │ │ │ ├── workflow_analysis.py
│ │ │ │ ├── workflow_execution.py
│ │ │ │ ├── workflow_linting.py
│ │ │ │ └── workflow_management.py
│ │ │ └── utils
│ │ │ ├── __init__.py
│ │ │ ├── aws_utils.py
│ │ │ ├── s3_utils.py
│ │ │ └── validation_utils.py
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── docs
│ │ │ └── workflow_linting.md
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── conftest.py
│ │ │ ├── test_aws_utils.py
│ │ │ ├── test_consts.py
│ │ │ ├── test_helper_tools.py
│ │ │ ├── test_init.py
│ │ │ ├── test_main.py
│ │ │ ├── test_models.py
│ │ │ ├── test_run_analysis.py
│ │ │ ├── test_s3_utils.py
│ │ │ ├── test_server.py
│ │ │ ├── test_troubleshooting.py
│ │ │ ├── test_workflow_analysis.py
│ │ │ ├── test_workflow_execution.py
│ │ │ ├── test_workflow_linting.py
│ │ │ ├── test_workflow_management.py
│ │ │ └── test_workflow_tools.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── aws-iot-sitewise-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── aws_iot_sitewise_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── client.py
│ │ │ ├── prompts
│ │ │ │ ├── __init__.py
│ │ │ │ ├── asset_hierarchy.py
│ │ │ │ ├── data_exploration.py
│ │ │ │ └── data_ingestion.py
│ │ │ ├── server.py
│ │ │ ├── tool_metadata.py
│ │ │ ├── tools
│ │ │ │ ├── __init__.py
│ │ │ │ ├── sitewise_access.py
│ │ │ │ ├── sitewise_asset_models.py
│ │ │ │ ├── sitewise_assets.py
│ │ │ │ ├── sitewise_data.py
│ │ │ │ └── sitewise_gateways.py
│ │ │ └── validation.py
│ │ ├── CHANGELOG.md
│ │ ├── DEVELOPMENT.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── examples
│ │ │ └── wind_farm_example.py
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── run_server.py
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── conftest.py
│ │ │ ├── test_init.py
│ │ │ ├── test_main.py
│ │ │ ├── test_server.py
│ │ │ ├── test_sitewise_access.py
│ │ │ ├── test_sitewise_asset_models.py
│ │ │ ├── test_sitewise_assets.py
│ │ │ ├── test_sitewise_data.py
│ │ │ ├── test_sitewise_gateways.py
│ │ │ └── test_validation.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── aws-knowledge-mcp-server
│ │ └── README.md
│ ├── aws-location-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── aws_location_server
│ │ │ ├── __init__.py
│ │ │ └── server.py
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── conftest.py
│ │ │ ├── test_server_integration.py
│ │ │ └── test_server.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── aws-msk-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── aws_msk_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── server.py
│ │ │ └── tools
│ │ │ ├── __init__.py
│ │ │ ├── common_functions
│ │ │ │ ├── __init__.py
│ │ │ │ ├── client_manager.py
│ │ │ │ └── common_functions.py
│ │ │ ├── logs_and_telemetry
│ │ │ │ ├── __init__.py
│ │ │ │ ├── cluster_metrics_tools.py
│ │ │ │ ├── list_customer_iam_access.py
│ │ │ │ └── metric_config.py
│ │ │ ├── mutate_cluster
│ │ │ │ ├── __init__.py
│ │ │ │ ├── batch_associate_scram_secret.py
│ │ │ │ ├── batch_disassociate_scram_secret.py
│ │ │ │ ├── create_cluster_v2.py
│ │ │ │ ├── put_cluster_policy.py
│ │ │ │ ├── reboot_broker.py
│ │ │ │ ├── update_broker_count.py
│ │ │ │ ├── update_broker_storage.py
│ │ │ │ ├── update_broker_type.py
│ │ │ │ ├── update_cluster_configuration.py
│ │ │ │ ├── update_monitoring.py
│ │ │ │ └── update_security.py
│ │ │ ├── mutate_config
│ │ │ │ ├── __init__.py
│ │ │ │ ├── create_configuration.py
│ │ │ │ ├── tag_resource.py
│ │ │ │ ├── untag_resource.py
│ │ │ │ └── update_configuration.py
│ │ │ ├── mutate_vpc
│ │ │ │ ├── __init__.py
│ │ │ │ ├── create_vpc_connection.py
│ │ │ │ ├── delete_vpc_connection.py
│ │ │ │ └── reject_client_vpc_connection.py
│ │ │ ├── read_cluster
│ │ │ │ ├── __init__.py
│ │ │ │ ├── describe_cluster_operation.py
│ │ │ │ ├── describe_cluster.py
│ │ │ │ ├── get_bootstrap_brokers.py
│ │ │ │ ├── get_cluster_policy.py
│ │ │ │ ├── get_compatible_kafka_versions.py
│ │ │ │ ├── list_client_vpc_connections.py
│ │ │ │ ├── list_cluster_operations.py
│ │ │ │ ├── list_nodes.py
│ │ │ │ └── list_scram_secrets.py
│ │ │ ├── read_config
│ │ │ │ ├── __init__.py
│ │ │ │ ├── describe_configuration_revision.py
│ │ │ │ ├── describe_configuration.py
│ │ │ │ ├── list_configuration_revisions.py
│ │ │ │ └── list_tags_for_resource.py
│ │ │ ├── read_global
│ │ │ │ ├── __init__.py
│ │ │ │ ├── list_clusters.py
│ │ │ │ ├── list_configurations.py
│ │ │ │ ├── list_kafka_versions.py
│ │ │ │ └── list_vpc_connections.py
│ │ │ ├── read_vpc
│ │ │ │ ├── __init__.py
│ │ │ │ └── describe_vpc_connection.py
│ │ │ └── static_tools
│ │ │ ├── __init__.py
│ │ │ └── cluster_best_practices.py
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── test_client_manager.py
│ │ │ ├── test_cluster_metrics_tools.py
│ │ │ ├── test_common_functions.py
│ │ │ ├── test_create_cluster_v2.py
│ │ │ ├── test_create_configuration.py
│ │ │ ├── test_create_vpc_connection.py
│ │ │ ├── test_delete_vpc_connection.py
│ │ │ ├── test_describe_cluster_operation.py
│ │ │ ├── test_describe_cluster.py
│ │ │ ├── test_describe_configuration_revision.py
│ │ │ ├── test_describe_configuration.py
│ │ │ ├── test_describe_vpc_connection.py
│ │ │ ├── test_get_bootstrap_brokers.py
│ │ │ ├── test_get_cluster_policy.py
│ │ │ ├── test_get_compatible_kafka_versions.py
│ │ │ ├── test_init.py
│ │ │ ├── test_list_client_vpc_connections.py
│ │ │ ├── test_list_cluster_operations.py
│ │ │ ├── test_list_clusters.py
│ │ │ ├── test_list_configuration_revisions.py
│ │ │ ├── test_list_configurations.py
│ │ │ ├── test_list_customer_iam_access.py
│ │ │ ├── test_list_kafka_versions.py
│ │ │ ├── test_list_nodes.py
│ │ │ ├── test_list_scram_secrets.py
│ │ │ ├── test_list_tags_for_resource.py
│ │ │ ├── test_list_vpc_connections.py
│ │ │ ├── test_logs_and_telemetry.py
│ │ │ ├── test_main.py
│ │ │ ├── test_mutate_cluster_init.py
│ │ │ ├── test_mutate_cluster_success_cases.py
│ │ │ ├── test_mutate_cluster.py
│ │ │ ├── test_mutate_config_init.py
│ │ │ ├── test_mutate_vpc_init.py
│ │ │ ├── test_read_cluster_init_updated.py
│ │ │ ├── test_read_cluster_init.py
│ │ │ ├── test_read_config_init.py
│ │ │ ├── test_read_global_init.py
│ │ │ ├── test_read_vpc_init.py
│ │ │ ├── test_reject_client_vpc_connection.py
│ │ │ ├── test_server.py
│ │ │ ├── test_static_tools_init.py
│ │ │ ├── test_tag_resource.py
│ │ │ ├── test_tool_descriptions.py
│ │ │ ├── test_untag_resource.py
│ │ │ └── test_update_configuration.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── aws-pricing-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── aws_pricing_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── cdk_analyzer.py
│ │ │ ├── consts.py
│ │ │ ├── helpers.py
│ │ │ ├── models.py
│ │ │ ├── pricing_client.py
│ │ │ ├── pricing_transformer.py
│ │ │ ├── report_generator.py
│ │ │ ├── server.py
│ │ │ ├── static
│ │ │ │ ├── __init__.py
│ │ │ │ ├── COST_REPORT_TEMPLATE.md
│ │ │ │ └── patterns
│ │ │ │ ├── __init__.py
│ │ │ │ └── BEDROCK.md
│ │ │ └── terraform_analyzer.py
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── conftest.py
│ │ │ ├── test_cdk_analyzer.py
│ │ │ ├── test_helpers.py
│ │ │ ├── test_pricing_client.py
│ │ │ ├── test_pricing_transformer.py
│ │ │ ├── test_report_generator.py
│ │ │ ├── test_server.py
│ │ │ └── test_terraform_analyzer.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── aws-serverless-mcp-server
│ │ ├── .pre-commit.config.yaml
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── aws_serverless_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── models.py
│ │ │ ├── resources
│ │ │ │ ├── __init__.py
│ │ │ │ ├── deployment_details.py
│ │ │ │ ├── deployment_list.py
│ │ │ │ ├── template_details.py
│ │ │ │ └── template_list.py
│ │ │ ├── server.py
│ │ │ ├── template
│ │ │ │ ├── __init__.py
│ │ │ │ ├── registry.py
│ │ │ │ ├── renderer.py
│ │ │ │ └── templates
│ │ │ │ ├── backend.j2
│ │ │ │ ├── frontend.j2
│ │ │ │ ├── fullstack.j2
│ │ │ │ └── README.md
│ │ │ ├── tools
│ │ │ │ ├── common
│ │ │ │ │ └── base_tool.py
│ │ │ │ ├── guidance
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── deploy_serverless_app_help.py
│ │ │ │ │ ├── get_iac_guidance.py
│ │ │ │ │ ├── get_lambda_event_schemas.py
│ │ │ │ │ ├── get_lambda_guidance.py
│ │ │ │ │ └── get_serverless_templates.py
│ │ │ │ ├── sam
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── sam_build.py
│ │ │ │ │ ├── sam_deploy.py
│ │ │ │ │ ├── sam_init.py
│ │ │ │ │ ├── sam_local_invoke.py
│ │ │ │ │ └── sam_logs.py
│ │ │ │ ├── schemas
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── describe_schema.py
│ │ │ │ │ ├── list_registries.py
│ │ │ │ │ └── search_schema.py
│ │ │ │ └── webapps
│ │ │ │ ├── __init__.py
│ │ │ │ ├── configure_domain.py
│ │ │ │ ├── deploy_webapp.py
│ │ │ │ ├── get_metrics.py
│ │ │ │ ├── update_webapp_frontend.py
│ │ │ │ ├── utils
│ │ │ │ │ ├── deploy_service.py
│ │ │ │ │ ├── frontend_uploader.py
│ │ │ │ │ └── startup_script_generator.py
│ │ │ │ └── webapp_deployment_help.py
│ │ │ └── utils
│ │ │ ├── __init__.py
│ │ │ ├── aws_client_helper.py
│ │ │ ├── cloudformation.py
│ │ │ ├── const.py
│ │ │ ├── deployment_manager.py
│ │ │ ├── github.py
│ │ │ └── process.py
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── conftest.py
│ │ │ ├── README.md
│ │ │ ├── test_cloudformation.py
│ │ │ ├── test_configure_domain.py
│ │ │ ├── test_deploy_serverless_app_help.py
│ │ │ ├── test_deploy_service.py
│ │ │ ├── test_deploy_webapp.py
│ │ │ ├── test_deployment_details.py
│ │ │ ├── test_deployment_help.py
│ │ │ ├── test_deployment_list.py
│ │ │ ├── test_deployment_manager.py
│ │ │ ├── test_frontend_uploader.py
│ │ │ ├── test_get_iac_guidance.py
│ │ │ ├── test_get_lambda_event_schemas.py
│ │ │ ├── test_get_lambda_guidance.py
│ │ │ ├── test_get_metrics.py
│ │ │ ├── test_get_serverless_templates.py
│ │ │ ├── test_github.py
│ │ │ ├── test_models.py
│ │ │ ├── test_process.py
│ │ │ ├── test_sam_build.py
│ │ │ ├── test_sam_deploy.py
│ │ │ ├── test_sam_init.py
│ │ │ ├── test_sam_local_invoke.py
│ │ │ ├── test_sam_logs.py
│ │ │ ├── test_schemas.py
│ │ │ ├── test_server.py
│ │ │ ├── test_startup_script_generator.py
│ │ │ ├── test_template_details.py
│ │ │ ├── test_template_list.py
│ │ │ ├── test_template_registry.py
│ │ │ ├── test_template_renderer.py
│ │ │ └── test_update_webapp_frontend.py
│ │ └── uv.lock
│ ├── aws-support-mcp-server
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── aws_support_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── client.py
│ │ │ ├── consts.py
│ │ │ ├── debug_helper.py
│ │ │ ├── errors.py
│ │ │ ├── formatters.py
│ │ │ ├── models.py
│ │ │ └── server.py
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── conftests.py
│ │ │ ├── test_aws_support_mcp_server.py
│ │ │ └── test_models.py
│ │ └── uv.lock
│ ├── bedrock-kb-retrieval-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── bedrock_kb_retrieval_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── knowledgebases
│ │ │ │ ├── __init__.py
│ │ │ │ ├── clients.py
│ │ │ │ ├── discovery.py
│ │ │ │ └── retrieval.py
│ │ │ ├── models.py
│ │ │ └── server.py
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── run_tests.sh
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── .gitignore
│ │ │ ├── conftest.py
│ │ │ ├── README.md
│ │ │ ├── test_clients.py
│ │ │ ├── test_discovery.py
│ │ │ ├── test_env_config.py
│ │ │ ├── test_models.py
│ │ │ ├── test_retrieval.py
│ │ │ └── test_server.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── billing-cost-management-mcp-server
│ │ ├── __init__.py
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── billing_cost_management_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── models.py
│ │ │ ├── prompts
│ │ │ │ ├── __init__.py
│ │ │ │ ├── decorator.py
│ │ │ │ ├── graviton_migration.py
│ │ │ │ ├── README.md
│ │ │ │ ├── savings_plans.py
│ │ │ │ └── types.py
│ │ │ ├── server.py
│ │ │ ├── templates
│ │ │ │ └── recommendation_templates
│ │ │ │ ├── ebs_volume.template
│ │ │ │ ├── ec2_asg.template
│ │ │ │ ├── ec2_instance.template
│ │ │ │ ├── ecs_service.template
│ │ │ │ ├── idle.template
│ │ │ │ ├── lambda_function.template
│ │ │ │ ├── rds_database.template
│ │ │ │ ├── reserved_instances.template
│ │ │ │ └── savings_plans.template
│ │ │ ├── tools
│ │ │ │ ├── __init__.py
│ │ │ │ ├── aws_pricing_operations.py
│ │ │ │ ├── aws_pricing_tools.py
│ │ │ │ ├── budget_tools.py
│ │ │ │ ├── compute_optimizer_tools.py
│ │ │ │ ├── cost_anomaly_tools.py
│ │ │ │ ├── cost_comparison_tools.py
│ │ │ │ ├── cost_explorer_operations.py
│ │ │ │ ├── cost_explorer_tools.py
│ │ │ │ ├── cost_optimization_hub_helpers.py
│ │ │ │ ├── cost_optimization_hub_tools.py
│ │ │ │ ├── free_tier_usage_tools.py
│ │ │ │ ├── recommendation_details_tools.py
│ │ │ │ ├── ri_performance_tools.py
│ │ │ │ ├── sp_performance_tools.py
│ │ │ │ ├── storage_lens_tools.py
│ │ │ │ └── unified_sql_tools.py
│ │ │ └── utilities
│ │ │ ├── __init__.py
│ │ │ ├── aws_service_base.py
│ │ │ ├── constants.py
│ │ │ ├── logging_utils.py
│ │ │ └── sql_utils.py
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── requirements.txt
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── conftest.py
│ │ │ ├── prompts
│ │ │ │ ├── __init__.py
│ │ │ │ └── test_prompts.py
│ │ │ ├── README.md
│ │ │ ├── test_models.py
│ │ │ ├── test_server.py
│ │ │ ├── tools
│ │ │ │ ├── __init__.py
│ │ │ │ ├── fixtures.py
│ │ │ │ ├── test_aws_pricing_tools.py
│ │ │ │ ├── test_budget_tools.py
│ │ │ │ ├── test_compute_optimizer_tools.py
│ │ │ │ ├── test_cost_anomaly_tools_enhanced.py
│ │ │ │ ├── test_cost_anomaly_tools.py
│ │ │ │ ├── test_cost_comparison_tools.py
│ │ │ │ ├── test_cost_explorer_operations.py
│ │ │ │ ├── test_cost_explorer_tools.py
│ │ │ │ ├── test_cost_optimization_hub_helpers.py
│ │ │ │ ├── test_cost_optimization_hub_tools.py
│ │ │ │ ├── test_free_tier_usage_tools_new.py
│ │ │ │ ├── test_recommendation_details_tools.py
│ │ │ │ ├── test_ri_performance_tools.py
│ │ │ │ ├── test_sp_performance_tools.py
│ │ │ │ ├── test_storage_lens_tools.py
│ │ │ │ └── test_unified_sql_tools.py
│ │ │ └── utilities
│ │ │ ├── test_aws_service_base.py
│ │ │ └── test_sql_utils.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── ccapi-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── ccapi_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── aws_client.py
│ │ │ ├── cloud_control_utils.py
│ │ │ ├── context.py
│ │ │ ├── errors.py
│ │ │ ├── iac_generator.py
│ │ │ ├── impl
│ │ │ │ ├── __init__.py
│ │ │ │ ├── tools
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── explanation.py
│ │ │ │ │ ├── infrastructure_generation.py
│ │ │ │ │ ├── resource_operations.py
│ │ │ │ │ ├── security_scanning.py
│ │ │ │ │ └── session_management.py
│ │ │ │ └── utils
│ │ │ │ ├── __init__.py
│ │ │ │ └── validation.py
│ │ │ ├── infrastructure_generator.py
│ │ │ ├── models
│ │ │ │ ├── __init__.py
│ │ │ │ └── models.py
│ │ │ ├── schema_manager.py
│ │ │ ├── server.py
│ │ │ └── static
│ │ │ └── __init__.py
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── run_tests.sh
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── test_aws_client.py
│ │ │ ├── test_checkov_install.py
│ │ │ ├── test_cloud_control_utils.py
│ │ │ ├── test_context.py
│ │ │ ├── test_errors.py
│ │ │ ├── test_explanation.py
│ │ │ ├── test_iac_generator.py
│ │ │ ├── test_infrastructure_generation.py
│ │ │ ├── test_infrastructure_generator.py
│ │ │ ├── test_models.py
│ │ │ ├── test_resource_operations.py
│ │ │ ├── test_schema_manager.py
│ │ │ ├── test_security_scanning.py
│ │ │ ├── test_server.py
│ │ │ ├── test_session_management.py
│ │ │ └── test_validation.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── cdk-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── cdk_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── core
│ │ │ │ ├── __init__.py
│ │ │ │ ├── resources.py
│ │ │ │ ├── search_utils.py
│ │ │ │ ├── server.py
│ │ │ │ └── tools.py
│ │ │ ├── data
│ │ │ │ ├── __init__.py
│ │ │ │ ├── cdk_nag_parser.py
│ │ │ │ ├── construct_descriptions.py
│ │ │ │ ├── genai_cdk_loader.py
│ │ │ │ ├── lambda_layer_parser.py
│ │ │ │ ├── lambda_powertools_loader.py
│ │ │ │ ├── schema_generator.py
│ │ │ │ └── solutions_constructs_parser.py
│ │ │ ├── server.py
│ │ │ └── static
│ │ │ ├── __init__.py
│ │ │ ├── CDK_GENERAL_GUIDANCE.md
│ │ │ ├── CDK_NAG_GUIDANCE.md
│ │ │ └── lambda_powertools
│ │ │ ├── bedrock.md
│ │ │ ├── cdk.md
│ │ │ ├── dependencies.md
│ │ │ ├── index.md
│ │ │ ├── insights.md
│ │ │ ├── logging.md
│ │ │ ├── metrics.md
│ │ │ └── tracing.md
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── core
│ │ │ │ ├── test_resources_enhanced.py
│ │ │ │ ├── test_resources.py
│ │ │ │ ├── test_search_utils.py
│ │ │ │ ├── test_server.py
│ │ │ │ └── test_tools.py
│ │ │ └── data
│ │ │ ├── test_cdk_nag_parser.py
│ │ │ ├── test_genai_cdk_loader.py
│ │ │ ├── test_lambda_powertools_loader.py
│ │ │ ├── test_schema_generator.py
│ │ │ └── test_solutions_constructs_parser.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── cfn-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── cfn_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── aws_client.py
│ │ │ ├── cloud_control_utils.py
│ │ │ ├── context.py
│ │ │ ├── errors.py
│ │ │ ├── iac_generator.py
│ │ │ ├── schema_manager.py
│ │ │ └── server.py
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── run_tests.sh
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── test_aws_client.py
│ │ │ ├── test_cloud_control_utils.py
│ │ │ ├── test_errors.py
│ │ │ ├── test_iac_generator.py
│ │ │ ├── test_init.py
│ │ │ ├── test_main.py
│ │ │ ├── test_schema_manager.py
│ │ │ └── test_server.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── cloudtrail-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── cloudtrail_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── common.py
│ │ │ ├── models.py
│ │ │ ├── server.py
│ │ │ └── tools.py
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── conftest.py
│ │ │ ├── test_init.py
│ │ │ ├── test_main.py
│ │ │ ├── test_models.py
│ │ │ ├── test_server.py
│ │ │ └── test_tools.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── cloudwatch-appsignals-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── cloudwatch_appsignals_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── audit_presentation_utils.py
│ │ │ ├── audit_utils.py
│ │ │ ├── aws_clients.py
│ │ │ ├── canary_utils.py
│ │ │ ├── server.py
│ │ │ ├── service_audit_utils.py
│ │ │ ├── service_tools.py
│ │ │ ├── sli_report_client.py
│ │ │ ├── slo_tools.py
│ │ │ ├── trace_tools.py
│ │ │ └── utils.py
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── conftest.py
│ │ │ ├── test_audit_presentation_utils.py
│ │ │ ├── test_audit_utils.py
│ │ │ ├── test_aws_profile.py
│ │ │ ├── test_canary_utils.py
│ │ │ ├── test_initialization.py
│ │ │ ├── test_server_audit_functions.py
│ │ │ ├── test_server_audit_tools.py
│ │ │ ├── test_server.py
│ │ │ ├── test_service_audit_utils.py
│ │ │ ├── test_service_tools_operations.py
│ │ │ ├── test_sli_report_client.py
│ │ │ ├── test_slo_tools.py
│ │ │ └── test_utils.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── cloudwatch-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── cloudwatch_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── cloudwatch_alarms
│ │ │ │ ├── models.py
│ │ │ │ └── tools.py
│ │ │ ├── cloudwatch_logs
│ │ │ │ ├── models.py
│ │ │ │ └── tools.py
│ │ │ ├── cloudwatch_metrics
│ │ │ │ ├── data
│ │ │ │ │ └── metric_metadata.json
│ │ │ │ ├── models.py
│ │ │ │ └── tools.py
│ │ │ ├── common.py
│ │ │ └── server.py
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── cloudwatch_alarms
│ │ │ │ ├── test_active_alarms.py
│ │ │ │ ├── test_alarm_history_integration.py
│ │ │ │ ├── test_alarm_history.py
│ │ │ │ └── test_alarms_error_handling.py
│ │ │ ├── cloudwatch_logs
│ │ │ │ ├── test_logs_error_handling.py
│ │ │ │ ├── test_logs_models.py
│ │ │ │ └── test_logs_server.py
│ │ │ ├── cloudwatch_metrics
│ │ │ │ ├── test_metrics_error_handling.py
│ │ │ │ ├── test_metrics_models.py
│ │ │ │ ├── test_metrics_server.py
│ │ │ │ └── test_validation_error.py
│ │ │ ├── test_common_and_server.py
│ │ │ ├── test_init.py
│ │ │ └── test_main.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── code-doc-gen-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── code_doc_gen_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── server.py
│ │ │ └── utils
│ │ │ ├── doc_generator.py
│ │ │ ├── models.py
│ │ │ ├── repomix_manager.py
│ │ │ └── templates.py
│ │ ├── CHANGELOG.md
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── test_doc_generator_edge_cases.py
│ │ │ ├── test_doc_generator.py
│ │ │ ├── test_init.py
│ │ │ ├── test_main.py
│ │ │ ├── test_repomix_manager_scenarios.py
│ │ │ ├── test_repomix_manager.py
│ │ │ ├── test_repomix_statistics.py
│ │ │ ├── test_server_extended.py
│ │ │ ├── test_server.py
│ │ │ └── test_templates.py
│ │ └── uv.lock
│ ├── core-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── core_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── server.py
│ │ │ └── static
│ │ │ ├── __init__.py
│ │ │ └── PROMPT_UNDERSTANDING.md
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── conftest.py
│ │ │ ├── README.md
│ │ │ ├── test_init.py
│ │ │ ├── test_main.py
│ │ │ ├── test_response_types.py
│ │ │ ├── test_server.py
│ │ │ └── test_static.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── cost-explorer-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── cost_explorer_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── comparison_handler.py
│ │ │ ├── constants.py
│ │ │ ├── cost_usage_handler.py
│ │ │ ├── forecasting_handler.py
│ │ │ ├── helpers.py
│ │ │ ├── metadata_handler.py
│ │ │ ├── models.py
│ │ │ ├── server.py
│ │ │ └── utility_handler.py
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── conftest.py
│ │ │ ├── test_comparison_handler.py
│ │ │ ├── test_cost_usage_handler.py
│ │ │ ├── test_forecasting_handler.py
│ │ │ ├── test_helpers.py
│ │ │ ├── test_metadata_handler.py
│ │ │ ├── test_models.py
│ │ │ ├── test_server.py
│ │ │ └── test_utility_handler.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── documentdb-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ └── documentdb_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── analytic_tools.py
│ │ │ ├── config.py
│ │ │ ├── connection_tools.py
│ │ │ ├── db_management_tools.py
│ │ │ ├── query_tools.py
│ │ │ ├── server.py
│ │ │ └── write_tools.py
│ │ ├── CHANGELOG.md
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── conftest.py
│ │ │ ├── test_analytic_tools.py
│ │ │ ├── test_connection_tools.py
│ │ │ ├── test_db_management_tools.py
│ │ │ ├── test_init.py
│ │ │ ├── test_main.py
│ │ │ ├── test_query_tools.py
│ │ │ └── test_write_tools.py
│ │ └── uv.lock
│ ├── dynamodb-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── dynamodb_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── common.py
│ │ │ ├── database_analysis_queries.py
│ │ │ ├── database_analyzers.py
│ │ │ ├── prompts
│ │ │ │ └── dynamodb_architect.md
│ │ │ └── server.py
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── conftest.py
│ │ │ ├── evals
│ │ │ │ ├── dynamic_evaluators.py
│ │ │ │ ├── evaluation_registry.py
│ │ │ │ ├── logging_config.py
│ │ │ │ ├── multiturn_evaluator.py
│ │ │ │ ├── README.md
│ │ │ │ ├── scenarios.py
│ │ │ │ └── test_dspy_evals.py
│ │ │ ├── test_dynamodb_server.py
│ │ │ └── test_source_db_integration.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── ecs-mcp-server
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── ecs_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── api
│ │ │ │ ├── __init__.py
│ │ │ │ ├── containerize.py
│ │ │ │ ├── delete.py
│ │ │ │ ├── ecs_troubleshooting.py
│ │ │ │ ├── infrastructure.py
│ │ │ │ ├── resource_management.py
│ │ │ │ ├── status.py
│ │ │ │ └── troubleshooting_tools
│ │ │ │ ├── __init__.py
│ │ │ │ ├── detect_image_pull_failures.py
│ │ │ │ ├── fetch_cloudformation_status.py
│ │ │ │ ├── fetch_network_configuration.py
│ │ │ │ ├── fetch_service_events.py
│ │ │ │ ├── fetch_task_failures.py
│ │ │ │ ├── fetch_task_logs.py
│ │ │ │ ├── get_ecs_troubleshooting_guidance.py
│ │ │ │ └── utils.py
│ │ │ ├── main.py
│ │ │ ├── modules
│ │ │ │ ├── __init__.py
│ │ │ │ ├── aws_knowledge_proxy.py
│ │ │ │ ├── containerize.py
│ │ │ │ ├── delete.py
│ │ │ │ ├── deployment_status.py
│ │ │ │ ├── infrastructure.py
│ │ │ │ ├── resource_management.py
│ │ │ │ └── troubleshooting.py
│ │ │ ├── templates
│ │ │ │ ├── ecr_infrastructure.json
│ │ │ │ └── ecs_infrastructure.json
│ │ │ └── utils
│ │ │ ├── arn_parser.py
│ │ │ ├── aws.py
│ │ │ ├── config.py
│ │ │ ├── docker.py
│ │ │ ├── security.py
│ │ │ ├── templates.py
│ │ │ └── time_utils.py
│ │ ├── DEVELOPMENT.md
│ │ ├── pyproject.toml
│ │ ├── pyrightconfig.json
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── conftest.py
│ │ │ ├── integ
│ │ │ │ └── mcp-inspector
│ │ │ │ ├── .gitignore
│ │ │ │ ├── README.md
│ │ │ │ ├── run-tests.sh
│ │ │ │ └── scenarios
│ │ │ │ ├── 01_comprehensive_troubleshooting
│ │ │ │ │ ├── 01_create.sh
│ │ │ │ │ ├── 02_validate.sh
│ │ │ │ │ ├── 03_cleanup.sh
│ │ │ │ │ ├── description.txt
│ │ │ │ │ └── utils
│ │ │ │ │ ├── mcp_helpers.sh
│ │ │ │ │ └── validation_helpers.sh
│ │ │ │ └── 02_test_knowledge_proxy_tools
│ │ │ │ ├── 01_create.sh
│ │ │ │ ├── 02_validate.sh
│ │ │ │ ├── 03_cleanup.sh
│ │ │ │ ├── description.txt
│ │ │ │ └── utils
│ │ │ │ ├── knowledge_validation_helpers.sh
│ │ │ │ └── mcp_knowledge_helpers.sh
│ │ │ ├── llm_testing
│ │ │ │ ├── invalid_cfn_template.yaml
│ │ │ │ ├── README.md
│ │ │ │ ├── run_tests.sh
│ │ │ │ ├── scenarios
│ │ │ │ │ ├── 01_cloudformation_failure
│ │ │ │ │ │ ├── 01_create.sh
│ │ │ │ │ │ ├── 02_validate.sh
│ │ │ │ │ │ ├── 03_prompts.txt
│ │ │ │ │ │ ├── 04_evaluation.md
│ │ │ │ │ │ ├── 05_cleanup.sh
│ │ │ │ │ │ └── description.txt
│ │ │ │ │ ├── 02_service_failure
│ │ │ │ │ │ ├── 01_create.sh
│ │ │ │ │ │ ├── 02_validate.sh
│ │ │ │ │ │ ├── 03_prompts.txt
│ │ │ │ │ │ ├── 04_evaluation.md
│ │ │ │ │ │ ├── 05_cleanup.sh
│ │ │ │ │ │ └── description.txt
│ │ │ │ │ ├── 03_task_exit_failure
│ │ │ │ │ │ ├── 01_create.sh
│ │ │ │ │ │ ├── 02_validate.sh
│ │ │ │ │ │ ├── 03_prompts.txt
│ │ │ │ │ │ ├── 04_evaluation.md
│ │ │ │ │ │ ├── 05_cleanup.sh
│ │ │ │ │ │ └── description.txt
│ │ │ │ │ ├── 04_network_configuration_failure
│ │ │ │ │ │ ├── 01_create.sh
│ │ │ │ │ │ ├── 02_validate.sh
│ │ │ │ │ │ ├── 03_prompts.txt
│ │ │ │ │ │ ├── 05_cleanup.sh
│ │ │ │ │ │ └── description.txt
│ │ │ │ │ ├── 05_resource_constraint_failure
│ │ │ │ │ │ ├── 01_create.sh
│ │ │ │ │ │ ├── 02_validate.sh
│ │ │ │ │ │ ├── 03_prompts.txt
│ │ │ │ │ │ ├── 05_cleanup.sh
│ │ │ │ │ │ └── description.txt
│ │ │ │ │ └── 06_load_balancer_failure
│ │ │ │ │ ├── 01_create.sh
│ │ │ │ │ ├── 02_validate.sh
│ │ │ │ │ ├── 03_prompts.txt
│ │ │ │ │ ├── 05_cleanup.sh
│ │ │ │ │ └── description.txt
│ │ │ │ ├── SCRIPT_IMPROVEMENTS.md
│ │ │ │ └── utils
│ │ │ │ ├── aws_helpers.sh
│ │ │ │ └── evaluation_template.md
│ │ │ └── unit
│ │ │ ├── __init__.py
│ │ │ ├── api
│ │ │ │ ├── conftest.py
│ │ │ │ ├── test_delete_api.py
│ │ │ │ ├── test_ecs_troubleshooting.py
│ │ │ │ ├── test_resource_management_api.py
│ │ │ │ └── troubleshooting_tools
│ │ │ │ └── test_fetch_network_configuration.py
│ │ │ ├── conftest.py
│ │ │ ├── modules
│ │ │ │ ├── test_aws_knowledge_proxy.py
│ │ │ │ └── test_resource_management_module.py
│ │ │ ├── test_aws_role_utils.py
│ │ │ ├── test_aws_utils.py
│ │ │ ├── test_containerize.py
│ │ │ ├── test_delete.py
│ │ │ ├── test_docker_utils.py
│ │ │ ├── test_docker_with_role.py
│ │ │ ├── test_image_pull_failure_extended.py
│ │ │ ├── test_image_pull_failure.py
│ │ │ ├── test_infrastructure_role.py
│ │ │ ├── test_infrastructure.py
│ │ │ ├── test_integration.py
│ │ │ ├── test_main.py
│ │ │ ├── test_resource_management_api_operation.py
│ │ │ ├── test_resource_management_tool.py
│ │ │ ├── test_resource_management.py
│ │ │ ├── test_security_integration.py
│ │ │ ├── test_status_pytest.py
│ │ │ ├── test_status.py
│ │ │ ├── troubleshooting_tools
│ │ │ │ ├── __init__.py
│ │ │ │ ├── conftest.py
│ │ │ │ ├── test_detect_image_pull_failures.py
│ │ │ │ ├── test_fetch_cloudformation_status.py
│ │ │ │ ├── test_fetch_service_events.py
│ │ │ │ ├── test_fetch_task_failures.py
│ │ │ │ ├── test_fetch_task_logs.py
│ │ │ │ ├── test_get_ecs_troubleshooting_guidance.py
│ │ │ │ ├── test_is_ecr_image_security.py
│ │ │ │ └── test_utils.py
│ │ │ └── utils
│ │ │ ├── __init__.py
│ │ │ ├── async_test_utils.py
│ │ │ ├── test_arn_parser.py
│ │ │ ├── test_config.py
│ │ │ ├── test_docker.py
│ │ │ ├── test_response_sanitization.py
│ │ │ ├── test_security_extended.py
│ │ │ ├── test_security.py
│ │ │ ├── test_templates.py
│ │ │ └── test_time_utils.py
│ │ └── uv.lock
│ ├── eks-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── eks_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── aws_helper.py
│ │ │ ├── cloudwatch_handler.py
│ │ │ ├── cloudwatch_metrics_guidance_handler.py
│ │ │ ├── consts.py
│ │ │ ├── data
│ │ │ │ └── eks_cloudwatch_metrics_guidance.json
│ │ │ ├── eks_kb_handler.py
│ │ │ ├── eks_stack_handler.py
│ │ │ ├── iam_handler.py
│ │ │ ├── insights_handler.py
│ │ │ ├── k8s_apis.py
│ │ │ ├── k8s_client_cache.py
│ │ │ ├── k8s_handler.py
│ │ │ ├── logging_helper.py
│ │ │ ├── models.py
│ │ │ ├── scripts
│ │ │ │ └── update_eks_cloudwatch_metrics_guidance.py
│ │ │ ├── server.py
│ │ │ ├── templates
│ │ │ │ ├── eks-templates
│ │ │ │ │ └── eks-with-vpc.yaml
│ │ │ │ └── k8s-templates
│ │ │ │ ├── deployment.yaml
│ │ │ │ └── service.yaml
│ │ │ └── vpc_config_handler.py
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── test_aws_helper.py
│ │ │ ├── test_cloudwatch_handler.py
│ │ │ ├── test_cloudwatch_metrics_guidance_handler.py
│ │ │ ├── test_eks_kb_handler.py
│ │ │ ├── test_eks_stack_handler.py
│ │ │ ├── test_iam_handler.py
│ │ │ ├── test_init.py
│ │ │ ├── test_insights_handler.py
│ │ │ ├── test_k8s_apis.py
│ │ │ ├── test_k8s_client_cache.py
│ │ │ ├── test_k8s_handler.py
│ │ │ ├── test_logging_helper.py
│ │ │ ├── test_main.py
│ │ │ ├── test_models.py
│ │ │ ├── test_server.py
│ │ │ └── test_vpc_config_handler.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── elasticache-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── elasticache_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── common
│ │ │ │ ├── __init__.py
│ │ │ │ ├── connection.py
│ │ │ │ ├── decorators.py
│ │ │ │ └── server.py
│ │ │ ├── context.py
│ │ │ ├── main.py
│ │ │ └── tools
│ │ │ ├── __init__.py
│ │ │ ├── cc
│ │ │ │ ├── __init__.py
│ │ │ │ ├── connect.py
│ │ │ │ ├── create.py
│ │ │ │ ├── delete.py
│ │ │ │ ├── describe.py
│ │ │ │ ├── modify.py
│ │ │ │ ├── parsers.py
│ │ │ │ └── processors.py
│ │ │ ├── ce
│ │ │ │ ├── __init__.py
│ │ │ │ └── get_cost_and_usage.py
│ │ │ ├── cw
│ │ │ │ ├── __init__.py
│ │ │ │ └── get_metric_statistics.py
│ │ │ ├── cwlogs
│ │ │ │ ├── __init__.py
│ │ │ │ ├── create_log_group.py
│ │ │ │ ├── describe_log_groups.py
│ │ │ │ ├── describe_log_streams.py
│ │ │ │ ├── filter_log_events.py
│ │ │ │ └── get_log_events.py
│ │ │ ├── firehose
│ │ │ │ ├── __init__.py
│ │ │ │ └── list_delivery_streams.py
│ │ │ ├── misc
│ │ │ │ ├── __init__.py
│ │ │ │ ├── batch_apply_update_action.py
│ │ │ │ ├── batch_stop_update_action.py
│ │ │ │ ├── describe_cache_engine_versions.py
│ │ │ │ ├── describe_engine_default_parameters.py
│ │ │ │ ├── describe_events.py
│ │ │ │ └── describe_service_updates.py
│ │ │ ├── rg
│ │ │ │ ├── __init__.py
│ │ │ │ ├── complete_migration.py
│ │ │ │ ├── connect.py
│ │ │ │ ├── create.py
│ │ │ │ ├── delete.py
│ │ │ │ ├── describe.py
│ │ │ │ ├── modify.py
│ │ │ │ ├── parsers.py
│ │ │ │ ├── processors.py
│ │ │ │ ├── start_migration.py
│ │ │ │ └── test_migration.py
│ │ │ └── serverless
│ │ │ ├── __init__.py
│ │ │ ├── connect.py
│ │ │ ├── create.py
│ │ │ ├── delete.py
│ │ │ ├── describe.py
│ │ │ ├── models.py
│ │ │ └── modify.py
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── test_connection.py
│ │ │ ├── test_decorators.py
│ │ │ ├── test_init.py
│ │ │ ├── test_main.py
│ │ │ └── tools
│ │ │ ├── cc
│ │ │ │ ├── __init__.py
│ │ │ │ ├── test_connect_additional.py
│ │ │ │ ├── test_connect_coverage_additional.py
│ │ │ │ ├── test_connect_coverage.py
│ │ │ │ ├── test_connect.py
│ │ │ │ ├── test_create_additional.py
│ │ │ │ ├── test_create.py
│ │ │ │ ├── test_delete.py
│ │ │ │ ├── test_describe.py
│ │ │ │ ├── test_modify.py
│ │ │ │ ├── test_parsers.py
│ │ │ │ └── test_processors.py
│ │ │ ├── ce
│ │ │ │ ├── __init__.py
│ │ │ │ └── test_get_cost_and_usage.py
│ │ │ ├── cw
│ │ │ │ └── test_get_metric_statistics.py
│ │ │ ├── cwlogs
│ │ │ │ ├── __init__.py
│ │ │ │ ├── test_create_log_group.py
│ │ │ │ ├── test_describe_log_groups.py
│ │ │ │ ├── test_describe_log_streams.py
│ │ │ │ ├── test_filter_log_events.py
│ │ │ │ └── test_get_log_events.py
│ │ │ ├── firehose
│ │ │ │ └── test_list_delivery_streams.py
│ │ │ ├── misc
│ │ │ │ ├── __init__.py
│ │ │ │ ├── test_batch_apply_update_action.py
│ │ │ │ ├── test_batch_stop_update_action.py
│ │ │ │ ├── test_describe_cache_engine_versions.py
│ │ │ │ ├── test_describe_engine_default_parameters.py
│ │ │ │ ├── test_describe_events.py
│ │ │ │ └── test_describe_service_updates.py
│ │ │ ├── rg
│ │ │ │ ├── __init__.py
│ │ │ │ ├── test_complete_migration.py
│ │ │ │ ├── test_connect_additional.py
│ │ │ │ ├── test_connect_coverage_additional.py
│ │ │ │ ├── test_connect_optional_fields.py
│ │ │ │ ├── test_connect_partial_coverage.py
│ │ │ │ ├── test_connect.py
│ │ │ │ ├── test_create.py
│ │ │ │ ├── test_delete.py
│ │ │ │ ├── test_describe.py
│ │ │ │ ├── test_modify.py
│ │ │ │ ├── test_parsers.py
│ │ │ │ ├── test_processors.py
│ │ │ │ ├── test_start_migration.py
│ │ │ │ └── test_test_migration.py
│ │ │ └── serverless
│ │ │ ├── test_connect_additional.py
│ │ │ ├── test_connect_coverage_additional.py
│ │ │ ├── test_connect_optional_fields.py
│ │ │ ├── test_connect.py
│ │ │ ├── test_create.py
│ │ │ ├── test_delete.py
│ │ │ ├── test_describe.py
│ │ │ └── test_modify.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── finch-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── finch_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── consts.py
│ │ │ ├── models.py
│ │ │ ├── server.py
│ │ │ └── utils
│ │ │ ├── __init__.py
│ │ │ ├── build.py
│ │ │ ├── common.py
│ │ │ ├── ecr.py
│ │ │ ├── push.py
│ │ │ └── vm.py
│ │ ├── CHANGELOG.md
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── test_cli_flags.py
│ │ │ ├── test_logging_configuration.py
│ │ │ ├── test_server.py
│ │ │ ├── test_utils_build.py
│ │ │ ├── test_utils_common.py
│ │ │ ├── test_utils_ecr.py
│ │ │ ├── test_utils_push.py
│ │ │ └── test_utils_vm.py
│ │ └── uv.lock
│ ├── frontend-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── frontend_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── server.py
│ │ │ ├── static
│ │ │ │ └── react
│ │ │ │ ├── essential-knowledge.md
│ │ │ │ └── troubleshooting.md
│ │ │ └── utils
│ │ │ ├── __init__.py
│ │ │ └── file_utils.py
│ │ ├── CHANGELOG.md
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── test_file_utils.py
│ │ │ ├── test_init.py
│ │ │ ├── test_main.py
│ │ │ └── test_server.py
│ │ └── uv.lock
│ ├── git-repo-research-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── git_repo_research_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── defaults.py
│ │ │ ├── embeddings.py
│ │ │ ├── github_search.py
│ │ │ ├── indexer.py
│ │ │ ├── models.py
│ │ │ ├── repository.py
│ │ │ ├── search.py
│ │ │ ├── server.py
│ │ │ └── utils.py
│ │ ├── CHANGELOG.md
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── run_tests.sh
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── conftest.py
│ │ │ ├── test_errors_repository.py
│ │ │ ├── test_github_search_edge_cases.py
│ │ │ ├── test_graphql_github_search.py
│ │ │ ├── test_local_repository.py
│ │ │ ├── test_repository_utils.py
│ │ │ ├── test_rest_github_search.py
│ │ │ ├── test_search.py
│ │ │ ├── test_server.py
│ │ │ └── test_url_repository.py
│ │ └── uv.lock
│ ├── healthlake-mcp-server
│ │ ├── .dockerignore
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── healthlake_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── fhir_operations.py
│ │ │ ├── main.py
│ │ │ ├── models.py
│ │ │ └── server.py
│ │ ├── CHANGELOG.md
│ │ ├── CONTRIBUTING.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── examples
│ │ │ ├── mcp_config.json
│ │ │ └── README.md
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── conftest.py
│ │ │ ├── test_fhir_client_comprehensive.py
│ │ │ ├── test_fhir_error_scenarios.py
│ │ │ ├── test_fhir_operations.py
│ │ │ ├── test_integration_mock_based.py
│ │ │ ├── test_main_edge_cases.py
│ │ │ ├── test_main.py
│ │ │ ├── test_mcp_integration_coverage.py
│ │ │ ├── test_models_edge_cases.py
│ │ │ ├── test_models.py
│ │ │ ├── test_readonly_mode.py
│ │ │ ├── test_server_core.py
│ │ │ ├── test_server_error_handling.py
│ │ │ ├── test_server_mcp_handlers.py
│ │ │ ├── test_server_toolhandler.py
│ │ │ └── test_server_validation.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── iam-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── iam_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── aws_client.py
│ │ │ ├── context.py
│ │ │ ├── errors.py
│ │ │ ├── models.py
│ │ │ └── server.py
│ │ ├── CHANGELOG.md
│ │ ├── DESIGN_COMPLIANCE.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── examples
│ │ │ ├── get_policy_document_example.py
│ │ │ └── inline_policy_demo.py
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── run_tests.sh
│ │ ├── tests
│ │ │ ├── test_context.py
│ │ │ ├── test_errors.py
│ │ │ ├── test_inline_policies.py
│ │ │ └── test_server.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── lambda-tool-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── lambda_tool_mcp_server
│ │ │ ├── __init__.py
│ │ │ └── server.py
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── examples
│ │ │ ├── README.md
│ │ │ └── sample_functions
│ │ │ ├── customer-create
│ │ │ │ └── app.py
│ │ │ ├── customer-id-from-email
│ │ │ │ └── app.py
│ │ │ ├── customer-info-from-id
│ │ │ │ └── app.py
│ │ │ └── template.yml
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── .gitignore
│ │ │ ├── conftest.py
│ │ │ ├── README.md
│ │ │ ├── test_format_lambda_response.py
│ │ │ ├── test_integration_coverage.py
│ │ │ ├── test_integration.py
│ │ │ ├── test_register_lambda_functions.py
│ │ │ ├── test_schema_integration.py
│ │ │ ├── test_server_coverage_additional.py
│ │ │ ├── test_server_coverage.py
│ │ │ └── test_server.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── mcp-lambda-handler
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ └── mcp_lambda_handler
│ │ │ ├── __init__.py
│ │ │ ├── mcp_lambda_handler.py
│ │ │ ├── session.py
│ │ │ └── types.py
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ └── test_lambda_handler.py
│ │ └── uv.lock
│ ├── memcached-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── memcached_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── common
│ │ │ │ ├── config.py
│ │ │ │ ├── connection.py
│ │ │ │ └── server.py
│ │ │ ├── context.py
│ │ │ ├── main.py
│ │ │ └── tools
│ │ │ └── cache.py
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── ELASTICACHECONNECT.md
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── test_cache_readonly.py
│ │ │ ├── test_cache.py
│ │ │ ├── test_connection.py
│ │ │ ├── test_init.py
│ │ │ └── test_main.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── mysql-mcp-server
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── mysql_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── mutable_sql_detector.py
│ │ │ └── server.py
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── conftest.py
│ │ │ └── test_server.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── nova-canvas-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── nova_canvas_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── consts.py
│ │ │ ├── models.py
│ │ │ ├── novacanvas.py
│ │ │ └── server.py
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── .gitignore
│ │ │ ├── conftest.py
│ │ │ ├── README.md
│ │ │ ├── test_models.py
│ │ │ ├── test_novacanvas.py
│ │ │ └── test_server.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── openapi-mcp-server
│ │ ├── .coveragerc
│ │ ├── .dockerignore
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── AUTHENTICATION.md
│ │ ├── AWS_BEST_PRACTICES.md
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── openapi_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── api
│ │ │ │ ├── __init__.py
│ │ │ │ └── config.py
│ │ │ ├── auth
│ │ │ │ ├── __init__.py
│ │ │ │ ├── api_key_auth.py
│ │ │ │ ├── auth_cache.py
│ │ │ │ ├── auth_errors.py
│ │ │ │ ├── auth_factory.py
│ │ │ │ ├── auth_protocol.py
│ │ │ │ ├── auth_provider.py
│ │ │ │ ├── base_auth.py
│ │ │ │ ├── basic_auth.py
│ │ │ │ ├── bearer_auth.py
│ │ │ │ ├── cognito_auth.py
│ │ │ │ └── register.py
│ │ │ ├── patch
│ │ │ │ └── __init__.py
│ │ │ ├── prompts
│ │ │ │ ├── __init__.py
│ │ │ │ ├── generators
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── operation_prompts.py
│ │ │ │ │ └── workflow_prompts.py
│ │ │ │ ├── models.py
│ │ │ │ └── prompt_manager.py
│ │ │ ├── server.py
│ │ │ └── utils
│ │ │ ├── __init__.py
│ │ │ ├── cache_provider.py
│ │ │ ├── config.py
│ │ │ ├── error_handler.py
│ │ │ ├── http_client.py
│ │ │ ├── metrics_provider.py
│ │ │ ├── openapi_validator.py
│ │ │ └── openapi.py
│ │ ├── CHANGELOG.md
│ │ ├── DEPLOYMENT.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── OBSERVABILITY.md
│ │ ├── pyproject.toml
│ │ ├── pyrightconfig.json
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── api
│ │ │ │ └── test_config.py
│ │ │ ├── auth
│ │ │ │ ├── test_api_key_auth.py
│ │ │ │ ├── test_auth_cache.py
│ │ │ │ ├── test_auth_errors.py
│ │ │ │ ├── test_auth_factory_caching.py
│ │ │ │ ├── test_auth_factory_coverage.py
│ │ │ │ ├── test_auth_factory.py
│ │ │ │ ├── test_auth_protocol_additional.py
│ │ │ │ ├── test_auth_protocol_boost.py
│ │ │ │ ├── test_auth_protocol_coverage.py
│ │ │ │ ├── test_auth_protocol_extended.py
│ │ │ │ ├── test_auth_protocol_improved.py
│ │ │ │ ├── test_auth_protocol.py
│ │ │ │ ├── test_auth_provider_additional.py
│ │ │ │ ├── test_base_auth_coverage.py
│ │ │ │ ├── test_base_auth.py
│ │ │ │ ├── test_basic_auth.py
│ │ │ │ ├── test_bearer_auth.py
│ │ │ │ ├── test_cognito_auth_additional_coverage.py
│ │ │ │ ├── test_cognito_auth_boost_coverage.py
│ │ │ │ ├── test_cognito_auth_client_credentials.py
│ │ │ │ ├── test_cognito_auth_coverage_boost.py
│ │ │ │ ├── test_cognito_auth_exceptions.py
│ │ │ │ ├── test_cognito_auth.py
│ │ │ │ ├── test_register_coverage.py
│ │ │ │ └── test_register.py
│ │ │ ├── prompts
│ │ │ │ ├── standalone
│ │ │ │ │ ├── test_operation_prompt.py
│ │ │ │ │ ├── test_prompt_arguments.py
│ │ │ │ │ └── test_secure_operation_prompt.py
│ │ │ │ ├── test_mcp_prompt_manager_integration.py
│ │ │ │ ├── test_mcp_prompt_manager.py
│ │ │ │ ├── test_models_dict_method.py
│ │ │ │ ├── test_operation_prompts_extended.py
│ │ │ │ ├── test_prompt_manager_additional.py
│ │ │ │ ├── test_prompt_manager_comprehensive.py
│ │ │ │ ├── test_prompt_manager_coverage.py
│ │ │ │ └── test_prompt_registration.py
│ │ │ ├── README.md
│ │ │ ├── test_api_name.py
│ │ │ ├── test_cache_coverage_89.py
│ │ │ ├── test_client.py
│ │ │ ├── test_coverage_boost.py
│ │ │ ├── test_init.py
│ │ │ ├── test_main_extended.py
│ │ │ ├── test_main.py
│ │ │ ├── test_openapi_coverage_89.py
│ │ │ ├── test_server_auth_errors.py
│ │ │ ├── test_server_coverage_boost_2.py
│ │ │ ├── test_server_coverage_boost.py
│ │ │ ├── test_server_exception_handling.py
│ │ │ ├── test_server_extended.py
│ │ │ ├── test_server_httpx_version.py
│ │ │ ├── test_server_part1.py
│ │ │ ├── test_server_route_logging.py
│ │ │ ├── test_server_signal_handlers.py
│ │ │ ├── test_server.py
│ │ │ └── utils
│ │ │ ├── test_cache_provider.py
│ │ │ ├── test_error_handler_boost.py
│ │ │ ├── test_error_handler_extended.py
│ │ │ ├── test_error_handler_fix.py
│ │ │ ├── test_error_handler.py
│ │ │ ├── test_http_client_comprehensive.py
│ │ │ ├── test_http_client_extended.py
│ │ │ ├── test_http_client_extended2.py
│ │ │ ├── test_http_client_import_error.py
│ │ │ ├── test_http_client.py
│ │ │ ├── test_metrics_provider_decorators.py
│ │ │ ├── test_metrics_provider_extended2.py
│ │ │ ├── test_metrics_provider_prometheus.py
│ │ │ ├── test_metrics_provider.py
│ │ │ ├── test_openapi_validator.py
│ │ │ └── test_openapi.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── postgres-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── postgres_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── connection
│ │ │ │ ├── __init__.py
│ │ │ │ ├── abstract_db_connection.py
│ │ │ │ ├── db_connection_singleton.py
│ │ │ │ ├── psycopg_pool_connection.py
│ │ │ │ └── rds_api_connection.py
│ │ │ ├── mutable_sql_detector.py
│ │ │ └── server.py
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── conftest.py
│ │ │ ├── test_psycopg_connector.py
│ │ │ ├── test_server.py
│ │ │ └── test_singleton.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── prometheus-mcp-server
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── prometheus_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── consts.py
│ │ │ ├── models.py
│ │ │ └── server.py
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── conftest.py
│ │ │ ├── test_aws_credentials.py
│ │ │ ├── test_config_manager.py
│ │ │ ├── test_consts.py
│ │ │ ├── test_coverage_gaps.py
│ │ │ ├── test_coverage_improvement.py
│ │ │ ├── test_final_coverage.py
│ │ │ ├── test_init.py
│ │ │ ├── test_main.py
│ │ │ ├── test_models.py
│ │ │ ├── test_prometheus_client.py
│ │ │ ├── test_prometheus_connection.py
│ │ │ ├── test_security_validator.py
│ │ │ ├── test_server_coverage.py
│ │ │ ├── test_tools.py
│ │ │ └── test_workspace_config.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── redshift-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── redshift_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── consts.py
│ │ │ ├── models.py
│ │ │ ├── redshift.py
│ │ │ └── server.py
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── test_init.py
│ │ │ ├── test_main.py
│ │ │ ├── test_redshift.py
│ │ │ └── test_server.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── s3-tables-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── s3_tables_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── constants.py
│ │ │ ├── database.py
│ │ │ ├── engines
│ │ │ │ ├── __init__.py
│ │ │ │ └── pyiceberg.py
│ │ │ ├── file_processor
│ │ │ │ ├── __init__.py
│ │ │ │ ├── csv.py
│ │ │ │ ├── parquet.py
│ │ │ │ └── utils.py
│ │ │ ├── models.py
│ │ │ ├── namespaces.py
│ │ │ ├── resources.py
│ │ │ ├── s3_operations.py
│ │ │ ├── server.py
│ │ │ ├── table_buckets.py
│ │ │ ├── tables.py
│ │ │ └── utils.py
│ │ ├── CHANGELOG.md
│ │ ├── CONTEXT.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── test_csv.py
│ │ │ ├── test_database.py
│ │ │ ├── test_file_processor_utils.py
│ │ │ ├── test_init.py
│ │ │ ├── test_main.py
│ │ │ ├── test_namespaces.py
│ │ │ ├── test_parquet.py
│ │ │ ├── test_pyiceberg.py
│ │ │ ├── test_resources.py
│ │ │ ├── test_s3_operations.py
│ │ │ ├── test_server.py
│ │ │ ├── test_table_buckets.py
│ │ │ ├── test_tables.py
│ │ │ └── test_utils.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── stepfunctions-tool-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── stepfunctions_tool_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── aws_helper.py
│ │ │ └── server.py
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── .gitignore
│ │ │ ├── README.md
│ │ │ ├── test_aws_helper.py
│ │ │ ├── test_create_state_machine_tool.py
│ │ │ ├── test_filter_state_machines_by_tag.py
│ │ │ ├── test_format_state_machine_response.py
│ │ │ ├── test_get_schema_arn_from_state_machine_arn.py
│ │ │ ├── test_get_schema_from_registry.py
│ │ │ ├── test_invoke_express_state_machine_impl.py
│ │ │ ├── test_invoke_standard_state_machine_impl.py
│ │ │ ├── test_main.py
│ │ │ ├── test_register_state_machines.py
│ │ │ ├── test_sanitize_tool_name.py
│ │ │ ├── test_server.py
│ │ │ └── test_validate_state_machine_name.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── syntheticdata-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── syntheticdata_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── pandas_interpreter.py
│ │ │ ├── server.py
│ │ │ └── storage
│ │ │ ├── __init__.py
│ │ │ ├── base.py
│ │ │ ├── loader.py
│ │ │ └── s3.py
│ │ ├── CHANGELOG.md
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── conftest.py
│ │ │ ├── test_constants.py
│ │ │ ├── test_pandas_interpreter.py
│ │ │ ├── test_server.py
│ │ │ └── test_storage
│ │ │ ├── __init__.py
│ │ │ ├── test_loader.py
│ │ │ └── test_s3.py
│ │ └── uv.lock
│ ├── terraform-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── terraform_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── impl
│ │ │ │ ├── resources
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── terraform_aws_provider_resources_listing.py
│ │ │ │ │ └── terraform_awscc_provider_resources_listing.py
│ │ │ │ └── tools
│ │ │ │ ├── __init__.py
│ │ │ │ ├── execute_terraform_command.py
│ │ │ │ ├── execute_terragrunt_command.py
│ │ │ │ ├── run_checkov_scan.py
│ │ │ │ ├── search_aws_provider_docs.py
│ │ │ │ ├── search_awscc_provider_docs.py
│ │ │ │ ├── search_specific_aws_ia_modules.py
│ │ │ │ ├── search_user_provided_module.py
│ │ │ │ └── utils.py
│ │ │ ├── models
│ │ │ │ ├── __init__.py
│ │ │ │ └── models.py
│ │ │ ├── scripts
│ │ │ │ ├── generate_aws_provider_resources.py
│ │ │ │ ├── generate_awscc_provider_resources.py
│ │ │ │ └── scrape_aws_terraform_best_practices.py
│ │ │ ├── server.py
│ │ │ └── static
│ │ │ ├── __init__.py
│ │ │ ├── AWS_PROVIDER_RESOURCES.md
│ │ │ ├── AWS_TERRAFORM_BEST_PRACTICES.md
│ │ │ ├── AWSCC_PROVIDER_RESOURCES.md
│ │ │ ├── MCP_INSTRUCTIONS.md
│ │ │ └── TERRAFORM_WORKFLOW_GUIDE.md
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── .gitignore
│ │ │ ├── conftest.py
│ │ │ ├── README.md
│ │ │ ├── test_command_impl.py
│ │ │ ├── test_execute_terraform_command.py
│ │ │ ├── test_execute_terragrunt_command.py
│ │ │ ├── test_models.py
│ │ │ ├── test_parameter_annotations.py
│ │ │ ├── test_resources.py
│ │ │ ├── test_run_checkov_scan.py
│ │ │ ├── test_search_user_provided_module.py
│ │ │ ├── test_server.py
│ │ │ ├── test_tool_implementations.py
│ │ │ ├── test_utils_additional.py
│ │ │ └── test_utils.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── timestream-for-influxdb-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── timestream_for_influxdb_mcp_server
│ │ │ ├── __init__.py
│ │ │ └── server.py
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── test_init.py
│ │ │ ├── test_main.py
│ │ │ └── test_server.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ ├── valkey-mcp-server
│ │ ├── .gitignore
│ │ ├── .python-version
│ │ ├── awslabs
│ │ │ ├── __init__.py
│ │ │ └── valkey_mcp_server
│ │ │ ├── __init__.py
│ │ │ ├── common
│ │ │ │ ├── __init__.py
│ │ │ │ ├── config.py
│ │ │ │ ├── connection.py
│ │ │ │ └── server.py
│ │ │ ├── context.py
│ │ │ ├── main.py
│ │ │ ├── tools
│ │ │ │ ├── __init__.py
│ │ │ │ ├── bitmap.py
│ │ │ │ ├── hash.py
│ │ │ │ ├── hyperloglog.py
│ │ │ │ ├── json.py
│ │ │ │ ├── list.py
│ │ │ │ ├── misc.py
│ │ │ │ ├── server_management.py
│ │ │ │ ├── set.py
│ │ │ │ ├── sorted_set.py
│ │ │ │ ├── stream.py
│ │ │ │ └── string.py
│ │ │ └── version.py
│ │ ├── CHANGELOG.md
│ │ ├── docker-healthcheck.sh
│ │ ├── Dockerfile
│ │ ├── ELASTICACHECONNECT.md
│ │ ├── LICENSE
│ │ ├── NOTICE
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── tests
│ │ │ ├── test_bitmap.py
│ │ │ ├── test_config.py
│ │ │ ├── test_connection.py
│ │ │ ├── test_hash.py
│ │ │ ├── test_hyperloglog.py
│ │ │ ├── test_init.py
│ │ │ ├── test_json_additional.py
│ │ │ ├── test_json_readonly.py
│ │ │ ├── test_json.py
│ │ │ ├── test_list_additional.py
│ │ │ ├── test_list_readonly.py
│ │ │ ├── test_list.py
│ │ │ ├── test_main.py
│ │ │ ├── test_misc.py
│ │ │ ├── test_server_management.py
│ │ │ ├── test_set_readonly.py
│ │ │ ├── test_set.py
│ │ │ ├── test_sorted_set_additional.py
│ │ │ ├── test_sorted_set_readonly.py
│ │ │ ├── test_sorted_set.py
│ │ │ ├── test_stream_additional.py
│ │ │ ├── test_stream_readonly.py
│ │ │ ├── test_stream.py
│ │ │ └── test_string.py
│ │ ├── uv-requirements.txt
│ │ └── uv.lock
│ └── well-architected-security-mcp-server
│ ├── .python-version
│ ├── awslabs
│ │ └── well_architected_security_mcp_server
│ │ ├── __init__.py
│ │ ├── consts.py
│ │ ├── server.py
│ │ └── util
│ │ ├── __init__.py
│ │ ├── network_security.py
│ │ ├── prompt_utils.py
│ │ ├── resource_utils.py
│ │ ├── security_services.py
│ │ └── storage_security.py
│ ├── PROMPT_TEMPLATE.md
│ ├── pyproject.toml
│ ├── README.md
│ ├── tests
│ │ ├── __init__.py
│ │ ├── conftest.py
│ │ ├── README.md
│ │ ├── test_access_analyzer_fix.py
│ │ ├── test_network_security_additional.py
│ │ ├── test_network_security.py
│ │ ├── test_prompt_utils_coverage.py
│ │ ├── test_prompt_utils.py
│ │ ├── test_resource_utils_fix.py
│ │ ├── test_resource_utils.py
│ │ ├── test_security_services_additional.py
│ │ ├── test_security_services_coverage.py
│ │ ├── test_security_services.py
│ │ ├── test_server_additional.py
│ │ ├── test_server_coverage.py
│ │ ├── test_server_prompts.py
│ │ ├── test_server_security_findings.py
│ │ ├── test_server.py
│ │ ├── test_storage_security_additional.py
│ │ ├── test_storage_security_comprehensive.py
│ │ ├── test_storage_security_edge_cases.py
│ │ ├── test_storage_security_recommendations.py
│ │ ├── test_storage_security.py
│ │ └── test_user_agent_config.py
│ └── uv.lock
└── VIBE_CODING_TIPS_TRICKS.md
```
# Files
--------------------------------------------------------------------------------
/src/cloudwatch-appsignals-mcp-server/tests/test_server.py:
--------------------------------------------------------------------------------
```python
1 | """Tests for CloudWatch Application Signals MCP Server."""
2 |
3 | import json
4 | import pytest
5 | from awslabs.cloudwatch_appsignals_mcp_server.server import _filter_operation_targets, main
6 | from awslabs.cloudwatch_appsignals_mcp_server.service_tools import (
7 | get_service_detail,
8 | list_monitored_services,
9 | query_service_metrics,
10 | )
11 | from awslabs.cloudwatch_appsignals_mcp_server.slo_tools import get_slo
12 | from awslabs.cloudwatch_appsignals_mcp_server.trace_tools import (
13 | check_transaction_search_enabled,
14 | get_trace_summaries_paginated,
15 | list_slis,
16 | query_sampled_traces,
17 | search_transaction_spans,
18 | )
19 | from awslabs.cloudwatch_appsignals_mcp_server.utils import remove_null_values
20 | from botocore.exceptions import ClientError
21 | from datetime import datetime, timedelta, timezone
22 | from unittest.mock import AsyncMock, MagicMock, patch
23 |
24 |
25 | @pytest.fixture(autouse=True)
26 | def mock_aws_clients():
27 | """Mock all AWS clients to prevent real API calls during tests."""
28 | # Create mock clients
29 | mock_logs_client = MagicMock()
30 | mock_appsignals_client = MagicMock()
31 | mock_cloudwatch_client = MagicMock()
32 | mock_xray_client = MagicMock()
33 | mock_synthetics_client = MagicMock()
34 | mock_s3_client = MagicMock()
35 |
36 | # Patch the clients in all modules where they're imported
37 | patches = [
38 | # Original aws_clients module
39 | patch(
40 | 'awslabs.cloudwatch_appsignals_mcp_server.aws_clients.logs_client', mock_logs_client
41 | ),
42 | patch(
43 | 'awslabs.cloudwatch_appsignals_mcp_server.aws_clients.appsignals_client',
44 | mock_appsignals_client,
45 | ),
46 | patch(
47 | 'awslabs.cloudwatch_appsignals_mcp_server.aws_clients.cloudwatch_client',
48 | mock_cloudwatch_client,
49 | ),
50 | patch(
51 | 'awslabs.cloudwatch_appsignals_mcp_server.aws_clients.xray_client', mock_xray_client
52 | ),
53 | patch(
54 | 'awslabs.cloudwatch_appsignals_mcp_server.aws_clients.synthetics_client',
55 | mock_synthetics_client,
56 | ),
57 | patch('awslabs.cloudwatch_appsignals_mcp_server.aws_clients.s3_client', mock_s3_client),
58 | # Service tools module (check what's actually imported)
59 | patch(
60 | 'awslabs.cloudwatch_appsignals_mcp_server.service_tools.appsignals_client',
61 | mock_appsignals_client,
62 | ),
63 | patch(
64 | 'awslabs.cloudwatch_appsignals_mcp_server.service_tools.cloudwatch_client',
65 | mock_cloudwatch_client,
66 | ),
67 | # SLO tools module (check what's actually imported)
68 | patch(
69 | 'awslabs.cloudwatch_appsignals_mcp_server.slo_tools.appsignals_client',
70 | mock_appsignals_client,
71 | ),
72 | # Trace tools module (logs_client, xray_client, and appsignals_client are imported)
73 | patch(
74 | 'awslabs.cloudwatch_appsignals_mcp_server.trace_tools.logs_client', mock_logs_client
75 | ),
76 | patch(
77 | 'awslabs.cloudwatch_appsignals_mcp_server.trace_tools.xray_client', mock_xray_client
78 | ),
79 | patch(
80 | 'awslabs.cloudwatch_appsignals_mcp_server.trace_tools.appsignals_client',
81 | mock_appsignals_client,
82 | ),
83 | # SLI report client module (appsignals_client and cloudwatch_client are imported)
84 | patch(
85 | 'awslabs.cloudwatch_appsignals_mcp_server.sli_report_client.appsignals_client',
86 | mock_appsignals_client,
87 | ),
88 | patch(
89 | 'awslabs.cloudwatch_appsignals_mcp_server.sli_report_client.cloudwatch_client',
90 | mock_cloudwatch_client,
91 | ),
92 | patch(
93 | 'awslabs.cloudwatch_appsignals_mcp_server.server.synthetics_client',
94 | mock_synthetics_client,
95 | ),
96 | patch('awslabs.cloudwatch_appsignals_mcp_server.server.s3_client', mock_s3_client),
97 | patch('awslabs.cloudwatch_appsignals_mcp_server.server.iam_client', MagicMock()),
98 | ]
99 |
100 | # Start all patches
101 | for p in patches:
102 | p.start()
103 |
104 | try:
105 | yield {
106 | 'logs_client': mock_logs_client,
107 | 'appsignals_client': mock_appsignals_client,
108 | 'cloudwatch_client': mock_cloudwatch_client,
109 | 'xray_client': mock_xray_client,
110 | 'synthetics_client': mock_synthetics_client,
111 | 's3_client': mock_s3_client,
112 | }
113 | finally:
114 | # Stop all patches
115 | for p in patches:
116 | p.stop()
117 |
118 |
119 | @pytest.fixture
120 | def mock_mcp():
121 | """Mock the FastMCP instance."""
122 | with patch('awslabs.cloudwatch_appsignals_mcp_server.server.mcp') as mock:
123 | yield mock
124 |
125 |
126 | @pytest.mark.asyncio
127 | async def test_list_monitored_services_success(mock_aws_clients):
128 | """Test successful listing of monitored services."""
129 | mock_response = {
130 | 'ServiceSummaries': [
131 | {
132 | 'KeyAttributes': {
133 | 'Name': 'test-service',
134 | 'Type': 'AWS::ECS::Service',
135 | 'Environment': 'production',
136 | }
137 | }
138 | ]
139 | }
140 |
141 | mock_aws_clients['appsignals_client'].list_services.return_value = mock_response
142 |
143 | result = await list_monitored_services()
144 |
145 | assert 'Application Signals Services (1 total)' in result
146 | assert 'test-service' in result
147 | assert 'AWS::ECS::Service' in result
148 |
149 |
150 | @pytest.mark.asyncio
151 | async def test_list_monitored_services_empty(mock_aws_clients):
152 | """Test when no services are found."""
153 | mock_response = {'ServiceSummaries': []}
154 |
155 | mock_aws_clients['appsignals_client'].list_services.return_value = mock_response
156 |
157 | result = await list_monitored_services()
158 |
159 | assert result == 'No services found in Application Signals.'
160 |
161 |
162 | @pytest.mark.asyncio
163 | async def test_get_service_detail_success(mock_aws_clients):
164 | """Test successful retrieval of service details."""
165 | mock_list_response = {
166 | 'ServiceSummaries': [
167 | {'KeyAttributes': {'Name': 'test-service', 'Type': 'AWS::ECS::Service'}}
168 | ]
169 | }
170 |
171 | mock_get_response = {
172 | 'Service': {
173 | 'KeyAttributes': {'Name': 'test-service', 'Type': 'AWS::ECS::Service'},
174 | 'AttributeMaps': [{'Platform': 'ECS', 'Application': 'test-app'}],
175 | 'MetricReferences': [
176 | {
177 | 'Namespace': 'AWS/ApplicationSignals',
178 | 'MetricName': 'Latency',
179 | 'MetricType': 'GAUGE',
180 | 'Dimensions': [{'Name': 'Service', 'Value': 'test-service'}],
181 | }
182 | ],
183 | 'LogGroupReferences': [{'Identifier': '/aws/ecs/test-service'}],
184 | }
185 | }
186 |
187 | mock_aws_clients['appsignals_client'].list_services.return_value = mock_list_response
188 | mock_aws_clients['appsignals_client'].get_service.return_value = mock_get_response
189 |
190 | result = await get_service_detail('test-service')
191 |
192 | assert 'Service Details: test-service' in result
193 | assert 'AWS::ECS::Service' in result
194 | assert 'Platform: ECS' in result
195 | assert 'AWS/ApplicationSignals/Latency' in result
196 | assert '/aws/ecs/test-service' in result
197 |
198 |
199 | @pytest.mark.asyncio
200 | async def test_get_service_detail_not_found(mock_aws_clients):
201 | """Test when service is not found."""
202 | mock_response = {'ServiceSummaries': []}
203 |
204 | mock_aws_clients['appsignals_client'].list_services.return_value = mock_response
205 |
206 | result = await get_service_detail('nonexistent-service')
207 |
208 | assert "Service 'nonexistent-service' not found" in result
209 |
210 |
211 | @pytest.mark.asyncio
212 | async def test_query_service_metrics_success(mock_aws_clients):
213 | """Test successful query of service metrics."""
214 | mock_list_response = {
215 | 'ServiceSummaries': [
216 | {'KeyAttributes': {'Name': 'test-service', 'Type': 'AWS::ECS::Service'}}
217 | ]
218 | }
219 |
220 | mock_get_response = {
221 | 'Service': {
222 | 'MetricReferences': [
223 | {
224 | 'Namespace': 'AWS/ApplicationSignals',
225 | 'MetricName': 'Latency',
226 | 'Dimensions': [{'Name': 'Service', 'Value': 'test-service'}],
227 | }
228 | ]
229 | }
230 | }
231 |
232 | mock_metric_response = {
233 | 'Datapoints': [
234 | {
235 | 'Timestamp': datetime.now(timezone.utc),
236 | 'Average': 100.5,
237 | 'p99': 150.2,
238 | 'Unit': 'Milliseconds',
239 | }
240 | ]
241 | }
242 |
243 | mock_aws_clients['appsignals_client'].list_services.return_value = mock_list_response
244 | mock_aws_clients['appsignals_client'].get_service.return_value = mock_get_response
245 | mock_aws_clients['cloudwatch_client'].get_metric_statistics.return_value = mock_metric_response
246 |
247 | result = await query_service_metrics(
248 | service_name='test-service',
249 | metric_name='Latency',
250 | statistic='Average',
251 | extended_statistic='p99',
252 | hours=1,
253 | )
254 |
255 | assert 'Metrics for test-service - Latency' in result
256 | assert 'Average Statistics:' in result
257 | assert 'p99 Statistics:' in result
258 |
259 |
260 | @pytest.mark.asyncio
261 | async def test_query_service_metrics_list_available(mock_aws_clients):
262 | """Test listing available metrics when no specific metric is requested."""
263 | mock_list_response = {
264 | 'ServiceSummaries': [
265 | {'KeyAttributes': {'Name': 'test-service', 'Type': 'AWS::ECS::Service'}}
266 | ]
267 | }
268 |
269 | mock_get_response = {
270 | 'Service': {
271 | 'MetricReferences': [
272 | {
273 | 'MetricName': 'Latency',
274 | 'Namespace': 'AWS/ApplicationSignals',
275 | 'MetricType': 'GAUGE',
276 | },
277 | {
278 | 'MetricName': 'Error',
279 | 'Namespace': 'AWS/ApplicationSignals',
280 | 'MetricType': 'COUNT',
281 | },
282 | ]
283 | }
284 | }
285 |
286 | mock_aws_clients['appsignals_client'].list_services.return_value = mock_list_response
287 | mock_aws_clients['appsignals_client'].get_service.return_value = mock_get_response
288 |
289 | result = await query_service_metrics(
290 | service_name='test-service',
291 | metric_name='', # Empty to list available metrics
292 | statistic='Average',
293 | extended_statistic='p99',
294 | hours=1,
295 | )
296 |
297 | assert "Available metrics for service 'test-service'" in result
298 | assert 'Latency' in result
299 | assert 'Error' in result
300 |
301 |
302 | @pytest.mark.asyncio
303 | async def test_get_slo_success(mock_aws_clients):
304 | """Test successful retrieval of SLO details."""
305 | mock_slo_response = {
306 | 'Slo': {
307 | 'Name': 'test-slo',
308 | 'Arn': 'arn:aws:application-signals:us-east-1:123456789012:slo/test-slo',
309 | 'Description': 'Test SLO for latency',
310 | 'EvaluationType': 'REQUEST_BASED',
311 | 'CreatedTime': '2024-01-01T00:00:00Z',
312 | 'LastUpdatedTime': '2024-01-02T00:00:00Z',
313 | 'Goal': {
314 | 'AttainmentGoal': 99.9,
315 | 'WarningThreshold': 99.0,
316 | 'Interval': {'RollingInterval': {'Duration': 7, 'DurationUnit': 'DAYS'}},
317 | },
318 | 'RequestBasedSli': {
319 | 'RequestBasedSliMetric': {
320 | 'KeyAttributes': {'Name': 'test-service', 'Type': 'AWS::ECS::Service'},
321 | 'OperationName': 'GET /api/test',
322 | 'MetricType': 'LATENCY',
323 | 'MetricDataQueries': [
324 | {
325 | 'Id': 'query1',
326 | 'MetricStat': {
327 | 'Metric': {
328 | 'Namespace': 'AWS/ApplicationSignals',
329 | 'MetricName': 'Latency',
330 | 'Dimensions': [{'Name': 'Service', 'Value': 'test-service'}],
331 | },
332 | 'Period': 60,
333 | 'Stat': 'Average',
334 | },
335 | }
336 | ],
337 | },
338 | 'MetricThreshold': 1000,
339 | 'ComparisonOperator': 'GreaterThan',
340 | },
341 | }
342 | }
343 |
344 | mock_aws_clients[
345 | 'appsignals_client'
346 | ].get_service_level_objective.return_value = mock_slo_response
347 |
348 | result = await get_slo('test-slo-id')
349 |
350 | assert 'Service Level Objective Details' in result
351 | assert 'test-slo' in result
352 | assert 'REQUEST_BASED' in result
353 | assert '99.9%' in result
354 | assert 'GET /api/test' in result
355 | assert 'annotation[aws.local.operation]="GET /api/test"' in result
356 |
357 |
358 | @pytest.mark.asyncio
359 | async def test_get_slo_not_found(mock_aws_clients):
360 | """Test when SLO is not found."""
361 | mock_aws_clients['appsignals_client'].get_service_level_objective.return_value = {'Slo': None}
362 |
363 | result = await get_slo('nonexistent-slo')
364 |
365 | assert 'No SLO found with ID: nonexistent-slo' in result
366 |
367 |
368 | @pytest.mark.asyncio
369 | async def test_search_transaction_spans_success(mock_aws_clients):
370 | """Test successful transaction search."""
371 | mock_query_response = {
372 | 'queryId': 'test-query-id',
373 | 'status': 'Complete',
374 | 'statistics': {'recordsMatched': 10},
375 | 'results': [
376 | [
377 | {'field': 'spanId', 'value': 'span1'},
378 | {'field': 'timestamp', 'value': '2024-01-01T00:00:00Z'},
379 | ]
380 | ],
381 | }
382 |
383 | with patch(
384 | 'awslabs.cloudwatch_appsignals_mcp_server.trace_tools.check_transaction_search_enabled'
385 | ) as mock_check:
386 | mock_check.return_value = (True, 'CloudWatchLogs', 'ACTIVE')
387 | mock_aws_clients['logs_client'].start_query.return_value = {'queryId': 'test-query-id'}
388 | mock_aws_clients['logs_client'].get_query_results.return_value = mock_query_response
389 |
390 | result = await search_transaction_spans(
391 | log_group_name='aws/spans',
392 | start_time='2024-01-01T00:00:00+00:00', # Fixed ISO format
393 | end_time='2024-01-01T01:00:00+00:00',
394 | query_string='fields @timestamp, spanId',
395 | limit=100,
396 | max_timeout=30,
397 | )
398 |
399 | assert result['status'] == 'Complete'
400 | assert result['queryId'] == 'test-query-id'
401 | assert len(result['results']) == 1
402 | assert result['results'][0]['spanId'] == 'span1'
403 |
404 |
405 | @pytest.mark.asyncio
406 | async def test_search_transaction_spans_not_enabled(mock_aws_clients):
407 | """Test when transaction search is not enabled."""
408 | with patch(
409 | 'awslabs.cloudwatch_appsignals_mcp_server.trace_tools.check_transaction_search_enabled'
410 | ) as mock_check:
411 | mock_check.return_value = (False, 'XRay', 'INACTIVE')
412 |
413 | result = await search_transaction_spans(
414 | log_group_name='',
415 | start_time='2024-01-01T00:00:00+00:00',
416 | end_time='2024-01-01T01:00:00+00:00',
417 | query_string='fields @timestamp',
418 | limit=None,
419 | max_timeout=30,
420 | )
421 |
422 | assert result['status'] == 'Transaction Search Not Available'
423 | assert not result['transaction_search_status']['enabled']
424 |
425 |
426 | @pytest.mark.asyncio
427 | async def test_list_slis_success(mock_aws_clients):
428 | """Test successful listing of SLI status."""
429 | mock_services_response = {
430 | 'ServiceSummaries': [
431 | {
432 | 'KeyAttributes': {
433 | 'Name': 'test-service',
434 | 'Type': 'AWS::ECS::Service',
435 | 'Environment': 'production',
436 | }
437 | }
438 | ]
439 | }
440 |
441 | # Mock boto3.client calls in SLIReportClient
442 | with patch('boto3.client') as mock_boto3_client:
443 | with patch(
444 | 'awslabs.cloudwatch_appsignals_mcp_server.trace_tools.check_transaction_search_enabled'
445 | ) as mock_check:
446 | # Configure boto3.client to return our mocked clients
447 | def boto3_client_side_effect(service_name, **kwargs):
448 | if service_name == 'application-signals':
449 | return mock_aws_clients['appsignals_client']
450 | elif service_name == 'cloudwatch':
451 | return mock_aws_clients['cloudwatch_client']
452 | else:
453 | return MagicMock()
454 |
455 | mock_boto3_client.side_effect = boto3_client_side_effect
456 |
457 | mock_aws_clients[
458 | 'appsignals_client'
459 | ].list_services.return_value = mock_services_response
460 | mock_check.return_value = (True, 'CloudWatchLogs', 'ACTIVE')
461 |
462 | # Mock SLO summaries response for SLIReportClient
463 | mock_slo_response = {
464 | 'SloSummaries': [
465 | {
466 | 'Name': 'test-slo',
467 | 'Arn': 'arn:aws:application-signals:us-east-1:123456789012:slo/test-slo',
468 | 'KeyAttributes': {'Name': 'test-service'},
469 | 'OperationName': 'GET /api',
470 | 'CreatedTime': datetime.now(timezone.utc),
471 | }
472 | ]
473 | }
474 | mock_aws_clients[
475 | 'appsignals_client'
476 | ].list_service_level_objectives.return_value = mock_slo_response
477 |
478 | # Mock metric data response showing breach
479 | mock_metric_response = {
480 | 'MetricDataResults': [
481 | {
482 | 'Id': 'slo0',
483 | 'Timestamps': [datetime.now(timezone.utc)],
484 | 'Values': [1.0], # Breach count > 0 indicates breach
485 | }
486 | ]
487 | }
488 | mock_aws_clients[
489 | 'cloudwatch_client'
490 | ].get_metric_data.return_value = mock_metric_response
491 |
492 | result = await list_slis(hours=24)
493 |
494 | assert 'SLI Status Report - Last 24 hours' in result
495 | assert 'Transaction Search: ENABLED' in result
496 | assert 'BREACHED SERVICES:' in result
497 | assert 'test-service' in result
498 |
499 |
500 | @pytest.mark.asyncio
501 | async def test_query_sampled_traces_success(mock_aws_clients):
502 | """Test successful query of sampled traces."""
503 | mock_traces = [
504 | {
505 | 'Id': 'trace1',
506 | 'Duration': 0.5,
507 | 'ResponseTime': 500,
508 | 'HasError': False,
509 | 'HasFault': True,
510 | 'HasThrottle': False,
511 | 'Http': {'HttpStatus': 500},
512 | 'FaultRootCauses': [
513 | {
514 | 'Services': [
515 | {
516 | 'Name': 'test-service',
517 | 'Exceptions': [{'Message': 'Internal server error'}],
518 | }
519 | ]
520 | }
521 | ],
522 | }
523 | ]
524 |
525 | with patch(
526 | 'awslabs.cloudwatch_appsignals_mcp_server.trace_tools.get_trace_summaries_paginated'
527 | ) as mock_get_traces:
528 | with patch(
529 | 'awslabs.cloudwatch_appsignals_mcp_server.trace_tools.check_transaction_search_enabled'
530 | ) as mock_check:
531 | mock_get_traces.return_value = mock_traces
532 | mock_check.return_value = (False, 'XRay', 'INACTIVE')
533 |
534 | result_json = await query_sampled_traces(
535 | start_time='2024-01-01T00:00:00Z',
536 | end_time='2024-01-01T01:00:00Z',
537 | filter_expression='service("test-service"){fault = true}',
538 | )
539 |
540 | result = json.loads(result_json)
541 | assert result['TraceCount'] == 1
542 | assert result['TraceSummaries'][0]['Id'] == 'trace1'
543 | assert result['TraceSummaries'][0]['HasFault'] is True
544 |
545 |
546 | @pytest.mark.asyncio
547 | async def test_query_sampled_traces_time_window_too_large(mock_aws_clients):
548 | """Test when time window is too large."""
549 | result_json = await query_sampled_traces(
550 | start_time='2024-01-01T00:00:00Z',
551 | end_time='2024-01-02T00:00:00Z', # 24 hours > 6 hours max
552 | filter_expression='service("test-service")',
553 | )
554 |
555 | result = json.loads(result_json)
556 | assert 'error' in result
557 | assert 'Time window too large' in result['error']
558 |
559 |
560 | def test_get_trace_summaries_paginated():
561 | """Test paginated trace retrieval."""
562 | mock_client = MagicMock()
563 | mock_responses = [
564 | {'TraceSummaries': [{'Id': 'trace1'}, {'Id': 'trace2'}], 'NextToken': 'token1'},
565 | {'TraceSummaries': [{'Id': 'trace3'}]},
566 | ]
567 | mock_client.get_trace_summaries.side_effect = mock_responses
568 |
569 | start_time = datetime.now(timezone.utc) - timedelta(hours=1)
570 | end_time = datetime.now(timezone.utc)
571 |
572 | traces = get_trace_summaries_paginated(
573 | mock_client, start_time, end_time, 'service("test")', max_traces=10
574 | )
575 |
576 | assert len(traces) == 3
577 | assert traces[0]['Id'] == 'trace1'
578 | assert traces[2]['Id'] == 'trace3'
579 |
580 |
581 | def test_get_trace_summaries_paginated_with_error():
582 | """Test paginated trace retrieval with error."""
583 | mock_client = MagicMock()
584 | mock_client.get_trace_summaries.side_effect = Exception('API Error')
585 |
586 | start_time = datetime.now(timezone.utc) - timedelta(hours=1)
587 | end_time = datetime.now(timezone.utc)
588 |
589 | traces = get_trace_summaries_paginated(
590 | mock_client, start_time, end_time, 'service("test")', max_traces=10
591 | )
592 |
593 | assert len(traces) == 0 # Should return empty list on error
594 |
595 |
596 | def test_check_transaction_search_enabled(mock_aws_clients):
597 | """Test checking transaction search status."""
598 | mock_aws_clients['xray_client'].get_trace_segment_destination.return_value = {
599 | 'Destination': 'CloudWatchLogs',
600 | 'Status': 'ACTIVE',
601 | }
602 |
603 | is_enabled, destination, status = check_transaction_search_enabled()
604 |
605 | assert is_enabled is True
606 | assert destination == 'CloudWatchLogs'
607 | assert status == 'ACTIVE'
608 |
609 |
610 | def test_check_transaction_search_enabled_not_active(mock_aws_clients):
611 | """Test checking transaction search when not active."""
612 | mock_aws_clients['xray_client'].get_trace_segment_destination.return_value = {
613 | 'Destination': 'XRay',
614 | 'Status': 'INACTIVE',
615 | }
616 |
617 | is_enabled, destination, status = check_transaction_search_enabled()
618 |
619 | assert is_enabled is False
620 | assert destination == 'XRay'
621 | assert status == 'INACTIVE'
622 |
623 |
624 | def test_check_transaction_search_enabled_error(mock_aws_clients):
625 | """Test checking transaction search with error."""
626 | mock_aws_clients['xray_client'].get_trace_segment_destination.side_effect = Exception(
627 | 'API Error'
628 | )
629 |
630 | is_enabled, destination, status = check_transaction_search_enabled()
631 |
632 | assert is_enabled is False
633 | assert destination == 'Unknown'
634 | assert status == 'Error'
635 |
636 |
637 | def test_remove_null_values():
638 | """Test remove_null_values function."""
639 | # Test with mix of None and non-None values
640 | input_dict = {
641 | 'key1': 'value1',
642 | 'key2': None,
643 | 'key3': 'value3',
644 | 'key4': None,
645 | 'key5': 0, # Should not be removed
646 | 'key6': '', # Should not be removed
647 | 'key7': False, # Should not be removed
648 | }
649 |
650 | result = remove_null_values(input_dict)
651 |
652 | assert result == {
653 | 'key1': 'value1',
654 | 'key3': 'value3',
655 | 'key5': 0,
656 | 'key6': '',
657 | 'key7': False,
658 | }
659 | assert 'key2' not in result
660 | assert 'key4' not in result
661 |
662 |
663 | @pytest.mark.asyncio
664 | async def test_list_monitored_services_client_error(mock_aws_clients):
665 | """Test ClientError handling in list_monitored_services."""
666 | mock_aws_clients['appsignals_client'].list_services.side_effect = ClientError(
667 | error_response={
668 | 'Error': {
669 | 'Code': 'AccessDeniedException',
670 | 'Message': 'User is not authorized to perform this action',
671 | }
672 | },
673 | operation_name='ListServices',
674 | )
675 |
676 | result = await list_monitored_services()
677 |
678 | assert 'AWS Error: User is not authorized to perform this action' in result
679 |
680 |
681 | @pytest.mark.asyncio
682 | async def test_list_monitored_services_general_exception(mock_aws_clients):
683 | """Test general exception handling in list_monitored_services."""
684 | mock_aws_clients['appsignals_client'].list_services.side_effect = Exception(
685 | 'Unexpected error occurred'
686 | )
687 |
688 | result = await list_monitored_services()
689 |
690 | assert 'Error: Unexpected error occurred' in result
691 |
692 |
693 | @pytest.mark.asyncio
694 | async def test_get_service_detail_client_error(mock_aws_clients):
695 | """Test ClientError handling in get_service_detail."""
696 | mock_list_response = {
697 | 'ServiceSummaries': [
698 | {'KeyAttributes': {'Name': 'test-service', 'Type': 'AWS::ECS::Service'}}
699 | ]
700 | }
701 |
702 | mock_aws_clients['appsignals_client'].list_services.return_value = mock_list_response
703 | mock_aws_clients['appsignals_client'].get_service.side_effect = ClientError(
704 | error_response={
705 | 'Error': {
706 | 'Code': 'ResourceNotFoundException',
707 | 'Message': 'Service not found in Application Signals',
708 | }
709 | },
710 | operation_name='GetService',
711 | )
712 |
713 | result = await get_service_detail('test-service')
714 |
715 | assert 'AWS Error: Service not found in Application Signals' in result
716 |
717 |
718 | @pytest.mark.asyncio
719 | async def test_get_service_detail_general_exception(mock_aws_clients):
720 | """Test general exception handling in get_service_detail."""
721 | mock_list_response = {
722 | 'ServiceSummaries': [
723 | {'KeyAttributes': {'Name': 'test-service', 'Type': 'AWS::ECS::Service'}}
724 | ]
725 | }
726 |
727 | mock_aws_clients['appsignals_client'].list_services.return_value = mock_list_response
728 | mock_aws_clients['appsignals_client'].get_service.side_effect = Exception(
729 | 'Unexpected error in get_service'
730 | )
731 |
732 | result = await get_service_detail('test-service')
733 |
734 | assert 'Error: Unexpected error in get_service' in result
735 |
736 |
737 | @pytest.mark.asyncio
738 | async def test_query_service_metrics_no_datapoints(mock_aws_clients):
739 | """Test query service metrics when no datapoints are returned."""
740 | mock_list_response = {
741 | 'ServiceSummaries': [
742 | {'KeyAttributes': {'Name': 'test-service', 'Type': 'AWS::ECS::Service'}}
743 | ]
744 | }
745 |
746 | mock_get_response = {
747 | 'Service': {
748 | 'MetricReferences': [
749 | {
750 | 'Namespace': 'AWS/ApplicationSignals',
751 | 'MetricName': 'Latency',
752 | 'Dimensions': [{'Name': 'Service', 'Value': 'test-service'}],
753 | }
754 | ]
755 | }
756 | }
757 |
758 | mock_metric_response = {'Datapoints': []}
759 |
760 | mock_aws_clients['appsignals_client'].list_services.return_value = mock_list_response
761 | mock_aws_clients['appsignals_client'].get_service.return_value = mock_get_response
762 | mock_aws_clients['cloudwatch_client'].get_metric_statistics.return_value = mock_metric_response
763 |
764 | result = await query_service_metrics(
765 | service_name='test-service',
766 | metric_name='Latency',
767 | statistic='Average',
768 | extended_statistic='p99',
769 | hours=1,
770 | )
771 |
772 | assert 'No data points found' in result
773 |
774 |
775 | @pytest.mark.asyncio
776 | async def test_search_transaction_spans_timeout(mock_aws_clients):
777 | """Test search transaction spans with timeout."""
778 | with patch(
779 | 'awslabs.cloudwatch_appsignals_mcp_server.trace_tools.check_transaction_search_enabled'
780 | ) as mock_check:
781 | with patch('awslabs.cloudwatch_appsignals_mcp_server.trace_tools.timer') as mock_timer:
782 | # Mock asyncio.sleep to prevent actual waiting
783 | with patch('asyncio.sleep', new_callable=AsyncMock):
784 | mock_check.return_value = (True, 'CloudWatchLogs', 'ACTIVE')
785 | mock_aws_clients['logs_client'].start_query.return_value = {
786 | 'queryId': 'test-query-id'
787 | }
788 | mock_aws_clients['logs_client'].get_query_results.return_value = {
789 | 'status': 'Running'
790 | }
791 |
792 | # Simulate timeout by making timer exceed max_timeout
793 | mock_timer.side_effect = [
794 | 0,
795 | 0,
796 | 0,
797 | 31,
798 | 31,
799 | ] # start_time_perf, poll_start, poll check 1, poll check 2
800 |
801 | result = await search_transaction_spans(
802 | log_group_name='',
803 | start_time='2024-01-01T00:00:00+00:00',
804 | end_time='2024-01-01T01:00:00+00:00',
805 | query_string='fields @timestamp',
806 | limit=None,
807 | max_timeout=30,
808 | )
809 |
810 | assert result['status'] == 'Polling Timeout'
811 | assert 'did not complete within 30 seconds' in result['message']
812 |
813 |
814 | def test_main_normal_execution(mock_mcp):
815 | """Test normal execution of main function."""
816 | main()
817 | mock_mcp.run.assert_called_once_with(transport='stdio')
818 |
819 |
820 | def test_main_keyboard_interrupt(mock_mcp):
821 | """Test KeyboardInterrupt handling in main function."""
822 | mock_mcp.run.side_effect = KeyboardInterrupt()
823 | # Should not raise an exception
824 | main()
825 | mock_mcp.run.assert_called_once_with(transport='stdio')
826 |
827 |
828 | def test_main_general_exception(mock_mcp):
829 | """Test general exception handling in main function."""
830 | mock_mcp.run.side_effect = Exception('Server error')
831 | with pytest.raises(Exception, match='Server error'):
832 | main()
833 | mock_mcp.run.assert_called_once_with(transport='stdio')
834 |
835 |
836 | @pytest.mark.asyncio
837 | async def test_get_slo_period_based(mock_aws_clients):
838 | """Test get_slo with period-based SLI configuration."""
839 | mock_slo_response = {
840 | 'Slo': {
841 | 'Name': 'test-slo-period',
842 | 'Arn': 'arn:aws:application-signals:us-east-1:123456789012:slo/test-slo',
843 | 'EvaluationType': 'PERIOD_BASED',
844 | 'Sli': {
845 | 'SliMetric': {
846 | 'KeyAttributes': {'Name': 'test-service', 'Type': 'AWS::Lambda::Function'},
847 | 'OperationName': 'ProcessOrder',
848 | 'MetricType': 'AVAILABILITY',
849 | 'DependencyConfig': {
850 | 'DependencyKeyAttributes': {'Name': 'payment-service'},
851 | 'DependencyOperationName': 'ProcessPayment',
852 | },
853 | },
854 | 'MetricThreshold': 0.99,
855 | 'ComparisonOperator': 'LessThan',
856 | },
857 | 'BurnRateConfigurations': [
858 | {'LookBackWindowMinutes': 5},
859 | {'LookBackWindowMinutes': 60},
860 | ],
861 | }
862 | }
863 |
864 | mock_aws_clients[
865 | 'appsignals_client'
866 | ].get_service_level_objective.return_value = mock_slo_response
867 |
868 | result = await get_slo('test-slo-period')
869 |
870 | assert 'PERIOD_BASED' in result
871 | assert 'ProcessOrder' in result
872 | assert 'annotation[aws.remote.operation]="ProcessPayment"' in result
873 | assert 'Burn Rate Configurations' in result
874 |
875 |
876 | @pytest.mark.asyncio
877 | async def test_list_slis_with_error_in_sli_client(mock_aws_clients):
878 | """Test list_slis when SLIReportClient throws error for some services."""
879 | mock_services_response = {
880 | 'ServiceSummaries': [
881 | {
882 | 'KeyAttributes': {
883 | 'Name': 'test-service-1',
884 | 'Type': 'AWS::ECS::Service',
885 | 'Environment': 'production',
886 | }
887 | },
888 | {
889 | 'KeyAttributes': {
890 | 'Name': 'test-service-2',
891 | 'Type': 'AWS::Lambda::Function',
892 | 'Environment': 'staging',
893 | }
894 | },
895 | ]
896 | }
897 |
898 | with patch(
899 | 'awslabs.cloudwatch_appsignals_mcp_server.trace_tools.SLIReportClient'
900 | ) as mock_sli_client:
901 | with patch(
902 | 'awslabs.cloudwatch_appsignals_mcp_server.trace_tools.check_transaction_search_enabled'
903 | ) as mock_check:
904 | mock_aws_clients[
905 | 'appsignals_client'
906 | ].list_services.return_value = mock_services_response
907 | mock_check.return_value = (False, 'XRay', 'INACTIVE')
908 |
909 | # First service succeeds, second fails
910 | mock_report = MagicMock()
911 | mock_report.breached_slo_count = 0
912 | mock_report.ok_slo_count = 3
913 | mock_report.total_slo_count = 3
914 | mock_report.sli_status = 'OK'
915 | mock_report.start_time = datetime.now(timezone.utc) - timedelta(hours=24)
916 | mock_report.end_time = datetime.now(timezone.utc)
917 |
918 | mock_sli_client.return_value.generate_sli_report.side_effect = [
919 | mock_report,
920 | Exception('Failed to get SLI report'),
921 | ]
922 |
923 | result = await list_slis(hours=24)
924 |
925 | assert 'Transaction Search: NOT ENABLED' in result
926 | assert 'HEALTHY SERVICES:' in result
927 | assert 'INSUFFICIENT DATA:' in result
928 | assert 'test-service-1' in result
929 | assert 'test-service-2' in result
930 |
931 |
932 | @pytest.mark.asyncio
933 | async def test_query_service_metrics_service_not_found(mock_aws_clients):
934 | """Test query service metrics when service is not found."""
935 | mock_list_response = {'ServiceSummaries': []}
936 |
937 | mock_aws_clients['appsignals_client'].list_services.return_value = mock_list_response
938 |
939 | result = await query_service_metrics(
940 | service_name='nonexistent-service',
941 | metric_name='Latency',
942 | statistic='Average',
943 | extended_statistic='p99',
944 | hours=1,
945 | )
946 |
947 | assert "Service 'nonexistent-service' not found" in result
948 |
949 |
950 | @pytest.mark.asyncio
951 | async def test_query_service_metrics_no_metrics(mock_aws_clients):
952 | """Test query service metrics when service has no metrics."""
953 | mock_list_response = {
954 | 'ServiceSummaries': [
955 | {'KeyAttributes': {'Name': 'test-service', 'Type': 'AWS::ECS::Service'}}
956 | ]
957 | }
958 |
959 | mock_get_response = {'Service': {'MetricReferences': []}}
960 |
961 | mock_aws_clients['appsignals_client'].list_services.return_value = mock_list_response
962 | mock_aws_clients['appsignals_client'].get_service.return_value = mock_get_response
963 |
964 | result = await query_service_metrics(
965 | service_name='test-service',
966 | metric_name='Latency',
967 | statistic='Average',
968 | extended_statistic='p99',
969 | hours=1,
970 | )
971 |
972 | assert "No metrics found for service 'test-service'" in result
973 |
974 |
975 | @pytest.mark.asyncio
976 | async def test_query_service_metrics_metric_not_found(mock_aws_clients):
977 | """Test query service metrics when specific metric is not found."""
978 | mock_list_response = {
979 | 'ServiceSummaries': [
980 | {'KeyAttributes': {'Name': 'test-service', 'Type': 'AWS::ECS::Service'}}
981 | ]
982 | }
983 |
984 | mock_get_response = {
985 | 'Service': {
986 | 'MetricReferences': [
987 | {
988 | 'Namespace': 'AWS/ApplicationSignals',
989 | 'MetricName': 'Error',
990 | 'Dimensions': [{'Name': 'Service', 'Value': 'test-service'}],
991 | }
992 | ]
993 | }
994 | }
995 |
996 | mock_aws_clients['appsignals_client'].list_services.return_value = mock_list_response
997 | mock_aws_clients['appsignals_client'].get_service.return_value = mock_get_response
998 |
999 | result = await query_service_metrics(
1000 | service_name='test-service',
1001 | metric_name='Latency', # Looking for Latency but only Error exists
1002 | statistic='Average',
1003 | extended_statistic='p99',
1004 | hours=1,
1005 | )
1006 |
1007 | assert "Metric 'Latency' not found" in result
1008 | assert 'Available: Error' in result
1009 |
1010 |
1011 | @pytest.mark.asyncio
1012 | async def test_query_service_metrics_client_error(mock_aws_clients):
1013 | """Test query service metrics with client error."""
1014 | mock_aws_clients['appsignals_client'].list_services.side_effect = ClientError(
1015 | error_response={
1016 | 'Error': {
1017 | 'Code': 'AccessDeniedException',
1018 | 'Message': 'User is not authorized',
1019 | }
1020 | },
1021 | operation_name='ListServices',
1022 | )
1023 |
1024 | result = await query_service_metrics(
1025 | service_name='test-service',
1026 | metric_name='Latency',
1027 | statistic='Average',
1028 | extended_statistic='p99',
1029 | hours=1,
1030 | )
1031 |
1032 | assert 'AWS Error: User is not authorized' in result
1033 |
1034 |
1035 | @pytest.mark.asyncio
1036 | async def test_search_transaction_spans_failed_query(mock_aws_clients):
1037 | """Test search transaction spans when query fails."""
1038 | with patch(
1039 | 'awslabs.cloudwatch_appsignals_mcp_server.trace_tools.check_transaction_search_enabled'
1040 | ) as mock_check:
1041 | mock_check.return_value = (True, 'CloudWatchLogs', 'ACTIVE')
1042 | mock_aws_clients['logs_client'].start_query.return_value = {'queryId': 'test-query-id'}
1043 | mock_aws_clients['logs_client'].get_query_results.return_value = {
1044 | 'queryId': 'test-query-id',
1045 | 'status': 'Failed',
1046 | 'statistics': {'error': 'Query syntax error'},
1047 | }
1048 |
1049 | result = await search_transaction_spans(
1050 | log_group_name='',
1051 | start_time='2024-01-01T00:00:00+00:00',
1052 | end_time='2024-01-01T01:00:00+00:00',
1053 | query_string='invalid query',
1054 | limit=None,
1055 | max_timeout=30,
1056 | )
1057 |
1058 | assert result['status'] == 'Failed'
1059 |
1060 |
1061 | @pytest.mark.asyncio
1062 | async def test_get_slo_client_error(mock_aws_clients):
1063 | """Test get_slo with client error."""
1064 | mock_aws_clients['appsignals_client'].get_service_level_objective.side_effect = ClientError(
1065 | error_response={
1066 | 'Error': {
1067 | 'Code': 'ResourceNotFoundException',
1068 | 'Message': 'SLO not found',
1069 | }
1070 | },
1071 | operation_name='GetServiceLevelObjective',
1072 | )
1073 |
1074 | result = await get_slo('test-slo-id')
1075 |
1076 | assert 'AWS Error: SLO not found' in result
1077 |
1078 |
1079 | @pytest.mark.asyncio
1080 | async def test_search_transaction_spans_empty_log_group(mock_aws_clients):
1081 | """Test search transaction spans with empty log group defaults to aws/spans."""
1082 | with patch(
1083 | 'awslabs.cloudwatch_appsignals_mcp_server.trace_tools.check_transaction_search_enabled'
1084 | ) as mock_check:
1085 | mock_check.return_value = (True, 'CloudWatchLogs', 'ACTIVE')
1086 | mock_aws_clients['logs_client'].start_query.return_value = {'queryId': 'test-query-id'}
1087 | mock_aws_clients['logs_client'].get_query_results.return_value = {
1088 | 'queryId': 'test-query-id',
1089 | 'status': 'Complete',
1090 | 'results': [],
1091 | }
1092 |
1093 | await search_transaction_spans(
1094 | log_group_name='', # Empty string should default to 'aws/spans'
1095 | start_time='2024-01-01T00:00:00+00:00',
1096 | end_time='2024-01-01T01:00:00+00:00',
1097 | query_string='fields @timestamp',
1098 | limit=None,
1099 | max_timeout=30,
1100 | )
1101 |
1102 | # Verify start_query was called with default 'aws/spans'
1103 | mock_aws_clients['logs_client'].start_query.assert_called()
1104 | call_args = mock_aws_clients['logs_client'].start_query.call_args[1]
1105 | assert 'aws/spans' in call_args['logGroupNames']
1106 |
1107 |
1108 | @pytest.mark.asyncio
1109 | async def test_list_slis_no_services(mock_aws_clients):
1110 | """Test list_slis when no services exist."""
1111 | mock_aws_clients['appsignals_client'].list_services.return_value = {'ServiceSummaries': []}
1112 |
1113 | result = await list_slis(hours=24)
1114 |
1115 | assert 'No services found in Application Signals.' in result
1116 |
1117 |
1118 | @pytest.mark.asyncio
1119 | async def test_get_slo_with_calendar_interval(mock_aws_clients):
1120 | """Test get_slo with calendar interval in goal."""
1121 | mock_slo_response = {
1122 | 'Slo': {
1123 | 'Name': 'test-slo-calendar',
1124 | 'Goal': {
1125 | 'AttainmentGoal': 99.5,
1126 | 'Interval': {
1127 | 'CalendarInterval': {
1128 | 'Duration': 1,
1129 | 'DurationUnit': 'MONTH',
1130 | 'StartTime': '2024-01-01T00:00:00Z',
1131 | }
1132 | },
1133 | },
1134 | }
1135 | }
1136 |
1137 | mock_aws_clients[
1138 | 'appsignals_client'
1139 | ].get_service_level_objective.return_value = mock_slo_response
1140 |
1141 | result = await get_slo('test-slo-calendar')
1142 |
1143 | assert 'Calendar 1 MONTH starting 2024-01-01T00:00:00Z' in result
1144 |
1145 |
1146 | @pytest.mark.asyncio
1147 | async def test_query_service_metrics_different_periods(mock_aws_clients):
1148 | """Test query service metrics with different time periods."""
1149 | # Test data for different hour ranges
1150 | test_cases = [
1151 | (2, 60), # 2 hours -> 1 minute period
1152 | (12, 300), # 12 hours -> 5 minute period
1153 | (48, 3600), # 48 hours -> 1 hour period
1154 | ]
1155 |
1156 | for hours, expected_period in test_cases:
1157 | mock_list_response = {
1158 | 'ServiceSummaries': [
1159 | {'KeyAttributes': {'Name': 'test-service', 'Type': 'AWS::ECS::Service'}}
1160 | ]
1161 | }
1162 |
1163 | mock_get_response = {
1164 | 'Service': {
1165 | 'MetricReferences': [
1166 | {
1167 | 'Namespace': 'AWS/ApplicationSignals',
1168 | 'MetricName': 'Latency',
1169 | 'Dimensions': [],
1170 | }
1171 | ]
1172 | }
1173 | }
1174 |
1175 | mock_metric_response = {
1176 | 'Datapoints': [{'Timestamp': datetime.now(timezone.utc), 'Average': 100.0}]
1177 | }
1178 |
1179 | mock_aws_clients['appsignals_client'].list_services.return_value = mock_list_response
1180 | mock_aws_clients['appsignals_client'].get_service.return_value = mock_get_response
1181 | mock_aws_clients[
1182 | 'cloudwatch_client'
1183 | ].get_metric_statistics.return_value = mock_metric_response
1184 |
1185 | await query_service_metrics(
1186 | service_name='test-service',
1187 | metric_name='Latency',
1188 | statistic='Average',
1189 | extended_statistic='p99',
1190 | hours=hours,
1191 | )
1192 |
1193 | # Verify the period was set correctly
1194 | call_args = mock_aws_clients['cloudwatch_client'].get_metric_statistics.call_args[1]
1195 | assert call_args['Period'] == expected_period
1196 |
1197 |
1198 | @pytest.mark.asyncio
1199 | async def test_query_service_metrics_general_exception(mock_aws_clients):
1200 | """Test query service metrics with unexpected exception."""
1201 | mock_aws_clients['appsignals_client'].list_services.side_effect = Exception('Unexpected error')
1202 |
1203 | result = await query_service_metrics(
1204 | service_name='test-service',
1205 | metric_name='Latency',
1206 | statistic='Average',
1207 | extended_statistic='p99',
1208 | hours=1,
1209 | )
1210 |
1211 | assert 'Error: Unexpected error' in result
1212 |
1213 |
1214 | @pytest.mark.asyncio
1215 | async def test_search_transaction_spans_general_exception(mock_aws_clients):
1216 | """Test search transaction spans with general exception."""
1217 | with patch(
1218 | 'awslabs.cloudwatch_appsignals_mcp_server.trace_tools.check_transaction_search_enabled'
1219 | ) as mock_check:
1220 | mock_check.return_value = (True, 'CloudWatchLogs', 'ACTIVE')
1221 | mock_aws_clients['logs_client'].start_query.side_effect = Exception('Query failed')
1222 |
1223 | with pytest.raises(Exception) as exc_info:
1224 | await search_transaction_spans(
1225 | log_group_name='aws/spans',
1226 | start_time='2024-01-01T00:00:00+00:00',
1227 | end_time='2024-01-01T01:00:00+00:00',
1228 | query_string='fields @timestamp',
1229 | limit=100,
1230 | max_timeout=30,
1231 | )
1232 |
1233 | assert 'Query failed' in str(exc_info.value)
1234 |
1235 |
1236 | @pytest.mark.asyncio
1237 | async def test_list_monitored_services_with_attributes_branch(mock_aws_clients):
1238 | """Test list_monitored_services with key attributes that trigger the branch."""
1239 | mock_response = {
1240 | 'ServiceSummaries': [
1241 | {
1242 | 'KeyAttributes': {} # Empty attributes to test the branch
1243 | }
1244 | ]
1245 | }
1246 |
1247 | mock_aws_clients['appsignals_client'].list_services.return_value = mock_response
1248 |
1249 | result = await list_monitored_services()
1250 |
1251 | assert 'Application Signals Services (1 total)' in result
1252 | assert 'Key Attributes:' not in result # Should not show when empty
1253 |
1254 |
1255 | @pytest.mark.asyncio
1256 | async def test_get_trace_summaries_paginated_with_limit(mock_aws_clients):
1257 | """Test get_trace_summaries_paginated when it hits the max_traces limit."""
1258 | # Mock responses with more traces than the limit
1259 | mock_response_1 = {
1260 | 'TraceSummaries': [{'Id': f'trace-{i}', 'Duration': 100} for i in range(10)],
1261 | 'NextToken': 'token1',
1262 | }
1263 | mock_response_2 = {
1264 | 'TraceSummaries': [{'Id': f'trace-{i}', 'Duration': 100} for i in range(10, 15)]
1265 | }
1266 |
1267 | mock_aws_clients['xray_client'].get_trace_summaries.side_effect = [
1268 | mock_response_1,
1269 | mock_response_2,
1270 | ]
1271 |
1272 | # Test with max_traces=12
1273 | traces = get_trace_summaries_paginated(
1274 | mock_aws_clients['xray_client'],
1275 | datetime.now(timezone.utc),
1276 | datetime.now(timezone.utc),
1277 | 'service("test")',
1278 | max_traces=12,
1279 | )
1280 |
1281 | # The function continues until it gets all traces from the current page
1282 | # before checking the limit, so we might get more than max_traces
1283 | assert len(traces) >= 12 # Should have at least the limit
1284 |
1285 |
1286 | @pytest.mark.asyncio
1287 | async def test_get_slo_with_period_based_sli_full_details(mock_aws_clients):
1288 | """Test get_slo with comprehensive period-based SLI configuration."""
1289 | mock_response = {
1290 | 'Slo': {
1291 | 'Name': 'test-slo',
1292 | 'Arn': 'arn:aws:slo:test',
1293 | 'Description': 'Test SLO',
1294 | 'EvaluationType': 'PERIOD_BASED',
1295 | 'CreatedTime': datetime.now(timezone.utc),
1296 | 'LastUpdatedTime': datetime.now(timezone.utc),
1297 | 'Goal': {
1298 | 'AttainmentGoal': 99.9,
1299 | 'WarningThreshold': 95,
1300 | 'Interval': {
1301 | 'CalendarInterval': {
1302 | 'Duration': 1,
1303 | 'DurationUnit': 'MONTH',
1304 | 'StartTime': datetime.now(timezone.utc),
1305 | }
1306 | },
1307 | },
1308 | 'Sli': {
1309 | 'SliMetric': {
1310 | 'KeyAttributes': {'Service': 'test-service', 'Environment': 'prod'},
1311 | 'OperationName': 'GetItem',
1312 | 'MetricType': 'LATENCY',
1313 | 'MetricDataQueries': [
1314 | {
1315 | 'Id': 'query1',
1316 | 'MetricStat': {
1317 | 'Metric': {
1318 | 'Namespace': 'AWS/ApplicationSignals',
1319 | 'MetricName': 'Latency',
1320 | 'Dimensions': [
1321 | {'Name': 'Service', 'Value': 'test-service'},
1322 | {'Name': 'Operation', 'Value': 'GetItem'},
1323 | ],
1324 | },
1325 | 'Period': 300,
1326 | 'Stat': 'p99',
1327 | 'Unit': 'Milliseconds',
1328 | },
1329 | 'ReturnData': True,
1330 | },
1331 | {'Id': 'query2', 'Expression': 'query1 * 2', 'ReturnData': False},
1332 | ],
1333 | 'DependencyConfig': {
1334 | 'DependencyKeyAttributes': {
1335 | 'RemoteService': 'downstream-service',
1336 | 'RemoteEnvironment': 'prod',
1337 | },
1338 | 'DependencyOperationName': 'ProcessRequest',
1339 | },
1340 | },
1341 | 'MetricThreshold': 1000,
1342 | 'ComparisonOperator': 'LessThan',
1343 | },
1344 | 'BurnRateConfigurations': [
1345 | {'LookBackWindowMinutes': 5},
1346 | {'LookBackWindowMinutes': 60},
1347 | ],
1348 | }
1349 | }
1350 |
1351 | mock_aws_clients['appsignals_client'].get_service_level_objective.return_value = mock_response
1352 |
1353 | result = await get_slo('test-slo-id')
1354 |
1355 | # Verify all sections are present
1356 | assert 'Service Level Objective Details' in result
1357 | assert 'Goal Configuration' in result
1358 | assert 'Calendar 1 MONTH' in result
1359 | assert 'Period-Based SLI Configuration' in result
1360 | assert 'Key Attributes:' in result
1361 | assert 'Service: test-service' in result
1362 | assert 'Operation Name: GetItem' in result
1363 | assert 'Metric Data Queries:' in result
1364 | assert 'Query ID: query1' in result
1365 | assert 'Namespace: AWS/ApplicationSignals' in result
1366 | assert 'Dimensions:' in result
1367 | assert 'Expression: query1 * 2' in result
1368 | assert 'ReturnData: False' in result
1369 | assert 'Dependency Configuration:' in result
1370 | assert 'RemoteService: downstream-service' in result
1371 | assert 'Dependency Operation: ProcessRequest' in result
1372 | assert 'Burn Rate Configurations:' in result
1373 |
1374 |
1375 | @pytest.mark.asyncio
1376 | async def test_get_slo_with_request_based_sli_full_details(mock_aws_clients):
1377 | """Test get_slo with comprehensive request-based SLI configuration."""
1378 | mock_response = {
1379 | 'Slo': {
1380 | 'Name': 'test-slo-rbs',
1381 | 'Arn': 'arn:aws:slo:test-rbs',
1382 | 'Goal': {
1383 | 'AttainmentGoal': 99.5,
1384 | 'Interval': {'RollingInterval': {'Duration': 7, 'DurationUnit': 'DAY'}},
1385 | },
1386 | 'RequestBasedSli': {
1387 | 'RequestBasedSliMetric': {
1388 | 'KeyAttributes': {'Service': 'api-service', 'Type': 'AWS::Lambda::Function'},
1389 | 'OperationName': 'ProcessOrder',
1390 | 'MetricType': 'AVAILABILITY',
1391 | 'MetricDataQueries': [
1392 | {
1393 | 'Id': 'success',
1394 | 'MetricStat': {
1395 | 'Metric': {
1396 | 'Namespace': 'AWS/Lambda',
1397 | 'MetricName': 'Success',
1398 | 'Dimensions': [
1399 | {'Name': 'FunctionName', 'Value': 'process-order'}
1400 | ],
1401 | },
1402 | 'Period': 60,
1403 | 'Stat': 'Sum',
1404 | },
1405 | },
1406 | {
1407 | 'Id': 'errors',
1408 | 'MetricStat': {
1409 | 'Metric': {
1410 | 'Namespace': 'AWS/Lambda',
1411 | 'MetricName': 'Errors',
1412 | 'Dimensions': [
1413 | {'Name': 'FunctionName', 'Value': 'process-order'}
1414 | ],
1415 | },
1416 | 'Period': 60,
1417 | 'Stat': 'Sum',
1418 | 'Unit': 'Count',
1419 | },
1420 | },
1421 | {'Id': 'availability', 'Expression': 'success / (success + errors) * 100'},
1422 | ],
1423 | 'DependencyConfig': {
1424 | 'DependencyKeyAttributes': {'Database': 'orders-db'},
1425 | 'DependencyOperationName': 'Query',
1426 | },
1427 | },
1428 | 'MetricThreshold': 99.0,
1429 | 'ComparisonOperator': 'GreaterThan',
1430 | },
1431 | }
1432 | }
1433 |
1434 | mock_aws_clients['appsignals_client'].get_service_level_objective.return_value = mock_response
1435 |
1436 | result = await get_slo('test-slo-rbs-id')
1437 |
1438 | # Verify request-based sections
1439 | assert 'Request-Based SLI Configuration:' in result
1440 | assert 'api-service' in result
1441 | assert 'ProcessOrder' in result
1442 | assert 'AVAILABILITY' in result
1443 | assert 'Expression: success / (success + errors) * 100' in result
1444 | assert 'Dependency Configuration:' in result
1445 | assert 'Database: orders-db' in result
1446 | assert 'Unit: Count' in result
1447 |
1448 |
1449 | @pytest.mark.asyncio
1450 | async def test_get_slo_general_exception(mock_aws_clients):
1451 | """Test get_slo with general exception."""
1452 | mock_aws_clients['appsignals_client'].get_service_level_objective.side_effect = Exception(
1453 | 'Unexpected error'
1454 | )
1455 |
1456 | result = await get_slo('test-slo-id')
1457 |
1458 | assert 'Error: Unexpected error' in result
1459 |
1460 |
1461 | @pytest.mark.asyncio
1462 | async def test_search_transaction_spans_with_none_log_group(mock_aws_clients):
1463 | """Test search_transaction_spans when log_group_name is None."""
1464 | with patch(
1465 | 'awslabs.cloudwatch_appsignals_mcp_server.trace_tools.check_transaction_search_enabled'
1466 | ) as mock_check:
1467 | mock_check.return_value = (True, 'CloudWatchLogs', 'ACTIVE')
1468 | mock_aws_clients['logs_client'].start_query.return_value = {'queryId': 'test-query-id'}
1469 | mock_aws_clients['logs_client'].get_query_results.return_value = {
1470 | 'queryId': 'test-query-id',
1471 | 'status': 'Complete',
1472 | 'results': [],
1473 | }
1474 |
1475 | # Pass None for log_group_name to test the default handling
1476 | await search_transaction_spans(
1477 | log_group_name=None, # type: ignore
1478 | start_time='2024-01-01T00:00:00+00:00',
1479 | end_time='2024-01-01T01:00:00+00:00',
1480 | query_string='fields @timestamp',
1481 | limit=100,
1482 | max_timeout=30,
1483 | )
1484 |
1485 | # Verify it used the default log group
1486 | call_args = mock_aws_clients['logs_client'].start_query.call_args[1]
1487 | assert 'aws/spans' in call_args['logGroupNames']
1488 |
1489 |
1490 | @pytest.mark.asyncio
1491 | async def test_search_transaction_spans_complete_with_statistics(mock_aws_clients):
1492 | """Test search_transaction_spans when query completes with detailed statistics."""
1493 | with patch(
1494 | 'awslabs.cloudwatch_appsignals_mcp_server.trace_tools.check_transaction_search_enabled'
1495 | ) as mock_check:
1496 | mock_check.return_value = (True, 'CloudWatchLogs', 'ACTIVE')
1497 | mock_aws_clients['logs_client'].start_query.return_value = {'queryId': 'test-query-id'}
1498 |
1499 | # First return Running, then Complete
1500 | mock_aws_clients['logs_client'].get_query_results.side_effect = [
1501 | {'queryId': 'test-query-id', 'status': 'Running'},
1502 | {
1503 | 'queryId': 'test-query-id',
1504 | 'status': 'Complete',
1505 | 'statistics': {
1506 | 'recordsMatched': 100,
1507 | 'recordsScanned': 1000,
1508 | 'bytesScanned': 50000,
1509 | },
1510 | 'results': [
1511 | [
1512 | {'field': 'spanId', 'value': 'span1'},
1513 | {'field': '@timestamp', 'value': '2024-01-01 00:00:00'},
1514 | ]
1515 | ],
1516 | },
1517 | ]
1518 |
1519 | result = await search_transaction_spans(
1520 | log_group_name='aws/spans',
1521 | start_time='2024-01-01T00:00:00+00:00',
1522 | end_time='2024-01-01T01:00:00+00:00',
1523 | query_string='fields @timestamp, spanId',
1524 | limit=100,
1525 | max_timeout=30,
1526 | )
1527 |
1528 | assert result['status'] == 'Complete'
1529 | assert result['statistics']['recordsMatched'] == 100
1530 | assert len(result['results']) == 1
1531 |
1532 |
1533 | @pytest.mark.asyncio
1534 | async def test_list_slis_general_exception(mock_aws_clients):
1535 | """Test list_slis with general exception."""
1536 | mock_aws_clients['appsignals_client'].list_services.side_effect = Exception(
1537 | 'Service unavailable'
1538 | )
1539 |
1540 | result = await list_slis(hours=24)
1541 |
1542 | assert 'Error getting SLI status: Service unavailable' in result
1543 |
1544 |
1545 | @pytest.mark.asyncio
1546 | async def test_query_sampled_traces_with_defaults(mock_aws_clients):
1547 | """Test query_sampled_traces with default start_time and end_time."""
1548 | mock_trace_response = {
1549 | 'TraceSummaries': [
1550 | {
1551 | 'Id': 'trace1',
1552 | 'Duration': 100,
1553 | 'HasError': True,
1554 | 'ErrorRootCauses': [
1555 | {
1556 | 'Services': [
1557 | {
1558 | 'Name': 'test-service',
1559 | 'Names': ['test-service'],
1560 | 'Type': 'AWS::ECS::Service',
1561 | 'AccountId': '123456789012',
1562 | 'EntityPath': [
1563 | {'Name': 'test-service', 'Coverage': 1.0, 'Remote': False}
1564 | ],
1565 | 'Inferred': False,
1566 | }
1567 | ],
1568 | 'ClientImpacting': True,
1569 | }
1570 | ],
1571 | }
1572 | ]
1573 | }
1574 |
1575 | with patch(
1576 | 'awslabs.cloudwatch_appsignals_mcp_server.trace_tools.get_trace_summaries_paginated'
1577 | ) as mock_paginated:
1578 | mock_paginated.return_value = mock_trace_response['TraceSummaries']
1579 |
1580 | # Call without start_time and end_time to test defaults
1581 | result_json = await query_sampled_traces(
1582 | filter_expression='service("test-service")',
1583 | start_time=None,
1584 | end_time=None,
1585 | region='us-east-1',
1586 | )
1587 |
1588 | result = json.loads(result_json)
1589 | assert result['TraceCount'] == 1
1590 | assert result['TraceSummaries'][0]['HasError'] is True
1591 |
1592 | # Verify the time window was set to 3 hours
1593 | call_args = mock_paginated.call_args[0]
1594 | time_diff = call_args[2] - call_args[1] # end_time - start_time
1595 | assert 2.9 < time_diff.total_seconds() / 3600 < 3.1 # Approximately 3 hours
1596 |
1597 |
1598 | @pytest.mark.asyncio
1599 | async def test_query_sampled_traces_with_annotations(mock_aws_clients):
1600 | """Test query_sampled_traces with annotations filtering."""
1601 | mock_trace = {
1602 | 'Id': 'trace1',
1603 | 'Duration': 100,
1604 | 'Annotations': {
1605 | 'aws.local.operation': 'GetItem',
1606 | 'aws.remote.operation': 'Query',
1607 | 'custom.field': 'should-be-filtered',
1608 | 'another.field': 'also-filtered',
1609 | },
1610 | 'Users': [
1611 | {'UserName': 'user1', 'ServiceIds': []},
1612 | {'UserName': 'user2', 'ServiceIds': []},
1613 | {'UserName': 'user3', 'ServiceIds': []}, # Should be limited to 2
1614 | ],
1615 | }
1616 |
1617 | with patch(
1618 | 'awslabs.cloudwatch_appsignals_mcp_server.trace_tools.get_trace_summaries_paginated'
1619 | ) as mock_paginated:
1620 | mock_paginated.return_value = [mock_trace]
1621 |
1622 | result_json = await query_sampled_traces(
1623 | start_time='2024-01-01T00:00:00Z',
1624 | end_time='2024-01-01T01:00:00Z',
1625 | filter_expression='service("test")',
1626 | )
1627 |
1628 | result = json.loads(result_json)
1629 | trace_summary = result['TraceSummaries'][0]
1630 |
1631 | # Check annotations were filtered
1632 | assert 'Annotations' in trace_summary
1633 | assert 'aws.local.operation' in trace_summary['Annotations']
1634 | assert 'aws.remote.operation' in trace_summary['Annotations']
1635 | assert 'custom.field' not in trace_summary['Annotations']
1636 |
1637 | # Check users were limited
1638 | assert len(trace_summary['Users']) == 2
1639 |
1640 |
1641 | @pytest.mark.asyncio
1642 | async def test_query_sampled_traces_with_fault_causes(mock_aws_clients):
1643 | """Test query_sampled_traces with fault root causes."""
1644 | mock_trace = {
1645 | 'Id': 'trace1',
1646 | 'Duration': 100,
1647 | 'HasFault': True,
1648 | 'FaultRootCauses': [
1649 | {'Services': [{'Name': 'service1', 'Exceptions': [{'Message': 'Test fault error'}]}]},
1650 | {'Services': [{'Name': 'service2'}]},
1651 | {'Services': [{'Name': 'service3'}]},
1652 | {'Services': [{'Name': 'service4'}]}, # Should be limited to 3
1653 | ],
1654 | 'ResponseTimeRootCauses': [{'Services': [{'Name': 'slow-service'}]}],
1655 | }
1656 |
1657 | with patch(
1658 | 'awslabs.cloudwatch_appsignals_mcp_server.trace_tools.get_trace_summaries_paginated'
1659 | ) as mock_paginated:
1660 | mock_paginated.return_value = [mock_trace]
1661 |
1662 | result_json = await query_sampled_traces(
1663 | start_time='2024-01-01T00:00:00Z', end_time='2024-01-01T01:00:00Z'
1664 | )
1665 |
1666 | result = json.loads(result_json)
1667 | trace_summary = result['TraceSummaries'][0]
1668 |
1669 | # Check root causes were limited to 3
1670 | assert len(trace_summary['FaultRootCauses']) == 3
1671 | assert 'ResponseTimeRootCauses' in trace_summary
1672 |
1673 |
1674 | @pytest.mark.asyncio
1675 | async def test_query_sampled_traces_general_exception(mock_aws_clients):
1676 | """Test query_sampled_traces with general exception."""
1677 | with patch(
1678 | 'awslabs.cloudwatch_appsignals_mcp_server.trace_tools.get_trace_summaries_paginated'
1679 | ) as mock_paginated:
1680 | mock_paginated.side_effect = Exception('Trace query failed')
1681 |
1682 | result_json = await query_sampled_traces(
1683 | start_time='2024-01-01T00:00:00Z', end_time='2024-01-01T01:00:00Z'
1684 | )
1685 |
1686 | result = json.loads(result_json)
1687 | assert 'error' in result
1688 | assert 'Trace query failed' in result['error']
1689 |
1690 |
1691 | @pytest.mark.asyncio
1692 | async def test_query_sampled_traces_datetime_conversion(mock_aws_clients):
1693 | """Test query_sampled_traces with datetime objects that need conversion."""
1694 | # The convert_datetime function in server.py only processes top-level fields,
1695 | # not nested datetime objects. Let's test with a datetime at the top level.
1696 | mock_trace = {
1697 | 'Id': 'trace1',
1698 | 'Duration': 100,
1699 | 'Http': {'HttpStatus': 200, 'HttpMethod': 'GET'},
1700 | 'StartTime': datetime.now(timezone.utc), # This will be processed by convert_datetime
1701 | 'EndTime': datetime.now(timezone.utc) + timedelta(minutes=1),
1702 | }
1703 |
1704 | with patch(
1705 | 'awslabs.cloudwatch_appsignals_mcp_server.trace_tools.get_trace_summaries_paginated'
1706 | ) as mock_paginated:
1707 | mock_paginated.return_value = [mock_trace]
1708 |
1709 | result_json = await query_sampled_traces(
1710 | start_time='2024-01-01T00:00:00Z', end_time='2024-01-01T01:00:00Z'
1711 | )
1712 |
1713 | # Should not raise JSON serialization error
1714 | result = json.loads(result_json)
1715 | assert result['TraceCount'] == 1
1716 | # The datetime fields should have been converted during processing
1717 | trace_summary = result['TraceSummaries'][0]
1718 | assert (
1719 | 'StartTime' not in trace_summary
1720 | ) # These fields are not included in the simplified output
1721 | assert 'EndTime' not in trace_summary
1722 |
1723 |
1724 | @pytest.mark.asyncio
1725 | async def test_query_sampled_traces_deduplication(mock_aws_clients):
1726 | """Test query_sampled_traces deduplicates traces with same fault message.
1727 |
1728 | Note: Only FaultRootCauses are deduplicated, not ErrorRootCauses.
1729 | This is because the primary use case is investigating server faults (5xx errors),
1730 | not client errors (4xx).
1731 | """
1732 | # Create 5 traces with the same fault message
1733 | mock_traces = [
1734 | {
1735 | 'Id': f'trace{i}',
1736 | 'Duration': 100 + i * 10,
1737 | 'ResponseTime': 95 + i * 10,
1738 | 'HasFault': True,
1739 | 'FaultRootCauses': [
1740 | {
1741 | 'Services': [
1742 | {
1743 | 'Name': 'test-service',
1744 | 'Exceptions': [{'Message': 'Database connection timeout'}],
1745 | }
1746 | ]
1747 | }
1748 | ],
1749 | }
1750 | for i in range(1, 6)
1751 | ]
1752 |
1753 | # Add 2 traces with ErrorRootCauses (these should NOT be deduplicated)
1754 | mock_traces.extend(
1755 | [
1756 | {
1757 | 'Id': 'trace6',
1758 | 'Duration': 200,
1759 | 'HasError': True,
1760 | 'ErrorRootCauses': [
1761 | {
1762 | 'Services': [
1763 | {
1764 | 'Name': 'api-service',
1765 | 'Exceptions': [{'Message': 'Invalid API key'}],
1766 | }
1767 | ]
1768 | }
1769 | ],
1770 | },
1771 | {
1772 | 'Id': 'trace7',
1773 | 'Duration': 210,
1774 | 'HasError': True,
1775 | 'ErrorRootCauses': [
1776 | {
1777 | 'Services': [
1778 | {
1779 | 'Name': 'api-service',
1780 | 'Exceptions': [{'Message': 'Invalid API key'}],
1781 | }
1782 | ]
1783 | }
1784 | ],
1785 | },
1786 | ]
1787 | )
1788 |
1789 | # Add 2 healthy traces
1790 | mock_traces.extend(
1791 | [
1792 | {
1793 | 'Id': 'trace8',
1794 | 'Duration': 50,
1795 | 'ResponseTime': 45,
1796 | 'HasError': False,
1797 | 'HasFault': False,
1798 | },
1799 | {
1800 | 'Id': 'trace9',
1801 | 'Duration': 55,
1802 | 'ResponseTime': 50,
1803 | 'HasError': False,
1804 | 'HasFault': False,
1805 | },
1806 | ]
1807 | )
1808 |
1809 | with patch(
1810 | 'awslabs.cloudwatch_appsignals_mcp_server.trace_tools.get_trace_summaries_paginated'
1811 | ) as mock_paginated:
1812 | mock_paginated.return_value = mock_traces
1813 |
1814 | result_json = await query_sampled_traces(
1815 | start_time='2024-01-01T00:00:00Z', end_time='2024-01-01T01:00:00Z'
1816 | )
1817 |
1818 | result = json.loads(result_json)
1819 |
1820 | # Verify deduplication worked - should only have 5 traces
1821 | # 1 for database timeout fault (deduplicated from 5)
1822 | # 2 for API key errors (NOT deduplicated - only faults are deduped)
1823 | # 2 healthy traces (not deduplicated)
1824 | assert result['TraceCount'] == 5
1825 | assert len(result['TraceSummaries']) == 5
1826 |
1827 | # Verify deduplication stats
1828 | assert 'DeduplicationStats' in result
1829 | assert result['DeduplicationStats']['OriginalTraceCount'] == 9
1830 | assert result['DeduplicationStats']['DuplicatesRemoved'] == 4 # 9 - 5 = 4
1831 | assert (
1832 | result['DeduplicationStats']['UniqueFaultMessages'] == 1
1833 | ) # Only counting FaultRootCauses
1834 |
1835 | # Find the trace with fault
1836 | db_trace = next(
1837 | (
1838 | t
1839 | for t in result['TraceSummaries']
1840 | if t.get('FaultRootCauses')
1841 | and any(
1842 | 'Database connection timeout' in str(s.get('Exceptions', []))
1843 | for cause in t['FaultRootCauses']
1844 | for s in cause.get('Services', [])
1845 | )
1846 | ),
1847 | None,
1848 | )
1849 | assert db_trace is not None
1850 | assert db_trace['HasFault'] is True
1851 |
1852 | # Verify both error traces are present (not deduplicated)
1853 | error_traces = [
1854 | t
1855 | for t in result['TraceSummaries']
1856 | if t.get('ErrorRootCauses')
1857 | and any(
1858 | 'Invalid API key' in str(s.get('Exceptions', []))
1859 | for cause in t['ErrorRootCauses']
1860 | for s in cause.get('Services', [])
1861 | )
1862 | ]
1863 | assert len(error_traces) == 2 # Both error traces should be kept
1864 | assert all(t['HasError'] is True for t in error_traces)
1865 |
1866 | # Verify healthy traces are included
1867 | healthy_count = sum(
1868 | 1
1869 | for t in result['TraceSummaries']
1870 | if not t.get('HasError') and not t.get('HasFault') and not t.get('HasThrottle')
1871 | )
1872 | assert healthy_count == 2
1873 |
1874 |
1875 | def test_main_success(mock_aws_clients):
1876 | """Test main function normal execution."""
1877 | with patch('awslabs.cloudwatch_appsignals_mcp_server.server.mcp') as mock_mcp:
1878 | main()
1879 | mock_mcp.run.assert_called_once_with(transport='stdio')
1880 |
1881 |
1882 | def test_main_exception(mock_aws_clients):
1883 | """Test main function with general exception."""
1884 | with patch('awslabs.cloudwatch_appsignals_mcp_server.server.mcp') as mock_mcp:
1885 | mock_mcp.run.side_effect = Exception('Server error')
1886 |
1887 | with pytest.raises(Exception) as exc_info:
1888 | main()
1889 |
1890 | assert 'Server error' in str(exc_info.value)
1891 |
1892 |
1893 | def test_main_entry_point(mock_aws_clients):
1894 | """Test the if __name__ == '__main__' entry point."""
1895 | # The __main__ block is simple and just calls main()
1896 | # We can't easily test it without executing the module
1897 | # So we'll just ensure the main() function works
1898 | # The actual line 1346 will be covered when the module is imported
1899 | # during normal test execution
1900 |
1901 | # Instead, let's just verify the main function exists and is callable
1902 | from awslabs.cloudwatch_appsignals_mcp_server.server import main
1903 |
1904 | assert callable(main)
1905 |
1906 | # And verify that running main with mocked mcp doesn't raise
1907 | with patch('awslabs.cloudwatch_appsignals_mcp_server.server.mcp') as mock_mcp:
1908 | mock_mcp.run.side_effect = KeyboardInterrupt()
1909 | # Should handle KeyboardInterrupt gracefully
1910 | main()
1911 |
1912 |
1913 | @pytest.mark.asyncio
1914 | async def test_analyze_canary_failures_no_runs(mock_aws_clients):
1915 | """Test analyze_canary_failures when no runs are found."""
1916 | from awslabs.cloudwatch_appsignals_mcp_server.server import analyze_canary_failures
1917 |
1918 | mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': []}
1919 | mock_aws_clients['synthetics_client'].get_canary.return_value = {
1920 | 'Canary': {'Name': 'test-canary'}
1921 | }
1922 |
1923 | result = await analyze_canary_failures('test-canary', 'us-east-1')
1924 |
1925 | assert 'No run history found for test-canary' in result
1926 |
1927 |
1928 | @pytest.mark.asyncio
1929 | async def test_analyze_canary_failures_healthy_canary(mock_aws_clients):
1930 | """Test analyze_canary_failures with healthy canary."""
1931 | from awslabs.cloudwatch_appsignals_mcp_server.server import analyze_canary_failures
1932 |
1933 | mock_runs = [
1934 | {
1935 | 'Id': 'run1',
1936 | 'Status': {'State': 'PASSED'},
1937 | 'Timeline': {'Started': '2024-01-01T00:00:00Z'},
1938 | }
1939 | ]
1940 |
1941 | mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
1942 | mock_aws_clients['synthetics_client'].get_canary.return_value = {
1943 | 'Canary': {'Name': 'test-canary'}
1944 | }
1945 |
1946 | with patch(
1947 | 'awslabs.cloudwatch_appsignals_mcp_server.server.get_canary_metrics_and_service_insights'
1948 | ) as mock_insights:
1949 | mock_insights.return_value = 'Telemetry insights available'
1950 |
1951 | result = await analyze_canary_failures('test-canary', 'us-east-1')
1952 |
1953 | assert 'Canary is healthy - no failures since last success' in result
1954 | assert '🔍 Comprehensive Failure Analysis for test-canary' in result
1955 |
1956 |
1957 | @pytest.mark.asyncio
1958 | async def test_analyze_canary_failures_telemetry_unavailable(mock_aws_clients):
1959 | """Test analyze_canary_failures when telemetry is unavailable."""
1960 | from awslabs.cloudwatch_appsignals_mcp_server.server import analyze_canary_failures
1961 |
1962 | mock_runs = [
1963 | {
1964 | 'Id': 'run1',
1965 | 'Status': {'State': 'PASSED'},
1966 | 'Timeline': {'Started': '2024-01-01T00:00:00Z'},
1967 | }
1968 | ]
1969 |
1970 | mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
1971 | mock_aws_clients['synthetics_client'].get_canary.return_value = {
1972 | 'Canary': {'Name': 'test-canary'}
1973 | }
1974 |
1975 | with patch(
1976 | 'awslabs.cloudwatch_appsignals_mcp_server.server.get_canary_metrics_and_service_insights'
1977 | ) as mock_insights:
1978 | mock_insights.side_effect = Exception('Telemetry API error')
1979 |
1980 | result = await analyze_canary_failures('test-canary', 'us-east-1')
1981 |
1982 | assert 'Telemetry API unavailable: Telemetry API error' in result
1983 |
1984 |
1985 | @pytest.mark.asyncio
1986 | async def test_analyze_canary_failures_with_failures(mock_aws_clients):
1987 | """Test analyze_canary_failures with actual failures."""
1988 | from awslabs.cloudwatch_appsignals_mcp_server.server import analyze_canary_failures
1989 |
1990 | mock_runs = [
1991 | {
1992 | 'Id': 'failed-run-1',
1993 | 'Status': {'State': 'FAILED', 'StateReason': 'Navigation timeout'},
1994 | 'Timeline': {'Started': '2024-01-01T01:00:00Z'},
1995 | },
1996 | {
1997 | 'Id': 'failed-run-2',
1998 | 'Status': {'State': 'FAILED', 'StateReason': 'Navigation timeout'},
1999 | 'Timeline': {'Started': '2024-01-01T00:30:00Z'},
2000 | },
2001 | {
2002 | 'Id': 'success-run',
2003 | 'Status': {'State': 'PASSED'},
2004 | 'Timeline': {'Started': '2024-01-01T00:00:00Z'},
2005 | },
2006 | ]
2007 |
2008 | mock_canary = {
2009 | 'Name': 'test-canary',
2010 | 'ArtifactS3Location': 's3://test-bucket/canary/us-east-1/test-canary',
2011 | }
2012 |
2013 | mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
2014 | mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}
2015 |
2016 | # Mock S3 artifacts
2017 | mock_aws_clients['s3_client'].list_objects_v2.return_value = {
2018 | 'Contents': [
2019 | {'Key': 'canary/us-east-1/test-canary/2024/01/01/test.har'},
2020 | {'Key': 'canary/us-east-1/test-canary/2024/01/01/screenshot.png'},
2021 | {'Key': 'canary/us-east-1/test-canary/2024/01/01/logs.txt'},
2022 | ]
2023 | }
2024 |
2025 | with patch(
2026 | 'awslabs.cloudwatch_appsignals_mcp_server.server.get_canary_metrics_and_service_insights'
2027 | ) as mock_insights:
2028 | with patch('awslabs.cloudwatch_appsignals_mcp_server.server.analyze_har_file') as mock_har:
2029 | with patch(
2030 | 'awslabs.cloudwatch_appsignals_mcp_server.server.analyze_screenshots'
2031 | ) as mock_screenshots:
2032 | with patch(
2033 | 'awslabs.cloudwatch_appsignals_mcp_server.server.analyze_log_files'
2034 | ) as mock_logs:
2035 | mock_insights.return_value = 'Telemetry insights'
2036 | mock_har.return_value = {
2037 | 'failed_requests': 2,
2038 | 'total_requests': 10,
2039 | 'request_details': [
2040 | {'url': 'https://example.com', 'status': 500, 'time': 1000}
2041 | ],
2042 | }
2043 | mock_screenshots.return_value = {'insights': ['Screenshot analysis']}
2044 | mock_logs.return_value = {'insights': ['Log analysis']}
2045 |
2046 | result = await analyze_canary_failures('test-canary', 'us-east-1')
2047 |
2048 | assert 'Found 2 consecutive failures since last success' in result
2049 | assert 'All failures have same cause: Navigation timeout' in result
2050 |
2051 |
2052 | @pytest.mark.asyncio
2053 | async def test_analyze_canary_failures_iam_analysis(mock_aws_clients):
2054 | """Test analyze_canary_failures with IAM-related failures."""
2055 | from awslabs.cloudwatch_appsignals_mcp_server.server import analyze_canary_failures
2056 |
2057 | mock_runs = [
2058 | {
2059 | 'Id': 'failed-run',
2060 | 'Status': {'State': 'FAILED', 'StateReason': 'Access denied'},
2061 | 'Timeline': {'Started': '2024-01-01T00:00:00Z'},
2062 | }
2063 | ]
2064 |
2065 | mock_canary = {'Name': 'test-canary', 'ArtifactS3Location': ''}
2066 |
2067 | mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
2068 | mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}
2069 |
2070 | with patch(
2071 | 'awslabs.cloudwatch_appsignals_mcp_server.server.get_canary_metrics_and_service_insights'
2072 | ) as mock_insights:
2073 | with patch(
2074 | 'awslabs.cloudwatch_appsignals_mcp_server.server.analyze_iam_role_and_policies'
2075 | ) as mock_iam:
2076 | with patch(
2077 | 'awslabs.cloudwatch_appsignals_mcp_server.server.check_resource_arns_correct'
2078 | ) as mock_arn:
2079 | mock_insights.return_value = 'Telemetry insights'
2080 | mock_iam.return_value = {
2081 | 'status': 'issues_found',
2082 | 'checks': {'role_exists': 'PASS', 'policies_attached': 'FAIL'},
2083 | 'issues_found': ['Missing S3 permissions'],
2084 | 'recommendations': ['Add S3 read permissions'],
2085 | }
2086 | mock_arn.return_value = {
2087 | 'correct': False,
2088 | 'error': 'Invalid bucket ARN',
2089 | 'issues': ['Bucket name mismatch'],
2090 | }
2091 |
2092 | result = await analyze_canary_failures('test-canary', 'us-east-1')
2093 |
2094 | assert 'RUNNING COMPREHENSIVE IAM ANALYSIS' in result
2095 | assert 'IAM Role Analysis Status: issues_found' in result
2096 | assert 'ALL IAM ISSUES FOUND (2 total):' in result
2097 | assert 'IAM Policy: Missing S3 permissions' in result
2098 | assert 'Resource ARN: Bucket name mismatch' in result
2099 |
2100 |
2101 | @pytest.mark.asyncio
2102 | async def test_analyze_canary_failures_enospc_error(mock_aws_clients):
2103 | """Test analyze_canary_failures with ENOSPC error."""
2104 | from awslabs.cloudwatch_appsignals_mcp_server.server import analyze_canary_failures
2105 |
2106 | mock_runs = [
2107 | {
2108 | 'Id': 'failed-run',
2109 | 'Status': {'State': 'FAILED', 'StateReason': 'ENOSPC: no space left on device'},
2110 | 'Timeline': {'Started': '2024-01-01T00:00:00Z'},
2111 | }
2112 | ]
2113 |
2114 | mock_canary = {'Name': 'test-canary', 'ArtifactS3Location': ''}
2115 |
2116 | mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
2117 | mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}
2118 |
2119 | with patch(
2120 | 'awslabs.cloudwatch_appsignals_mcp_server.server.get_canary_metrics_and_service_insights'
2121 | ) as mock_insights:
2122 | with patch(
2123 | 'awslabs.cloudwatch_appsignals_mcp_server.server.extract_disk_memory_usage_metrics'
2124 | ) as mock_metrics:
2125 | mock_insights.return_value = 'Telemetry insights'
2126 | mock_metrics.return_value = {
2127 | 'maxEphemeralStorageUsageInMb': 512.5,
2128 | 'maxEphemeralStorageUsagePercent': 95.2,
2129 | }
2130 |
2131 | result = await analyze_canary_failures('test-canary', 'us-east-1')
2132 |
2133 | assert 'DISK USAGE ROOT CAUSE ANALYSIS:' in result
2134 | assert 'Storage: 512.5 MB peak' in result
2135 | assert 'Usage: 95.2% peak' in result
2136 |
2137 |
2138 | @pytest.mark.asyncio
2139 | async def test_analyze_canary_failures_protocol_error(mock_aws_clients):
2140 | """Test analyze_canary_failures with protocol error."""
2141 | from awslabs.cloudwatch_appsignals_mcp_server.server import analyze_canary_failures
2142 |
2143 | mock_runs = [
2144 | {
2145 | 'Id': 'failed-run',
2146 | 'Status': {
2147 | 'State': 'FAILED',
2148 | 'StateReason': 'Protocol error (Target.activateTarget): Session closed',
2149 | },
2150 | 'Timeline': {'Started': '2024-01-01T00:00:00Z'},
2151 | }
2152 | ]
2153 |
2154 | mock_canary = {'Name': 'test-canary', 'ArtifactS3Location': ''}
2155 |
2156 | mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
2157 | mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}
2158 |
2159 | with patch(
2160 | 'awslabs.cloudwatch_appsignals_mcp_server.server.get_canary_metrics_and_service_insights'
2161 | ) as mock_insights:
2162 | with patch(
2163 | 'awslabs.cloudwatch_appsignals_mcp_server.server.extract_disk_memory_usage_metrics'
2164 | ) as mock_metrics:
2165 | mock_insights.return_value = 'Telemetry insights'
2166 | mock_metrics.return_value = {'maxSyntheticsMemoryUsageInMB': 256.8}
2167 |
2168 | result = await analyze_canary_failures('test-canary', 'us-east-1')
2169 |
2170 | assert 'MEMORY USAGE ROOT CAUSE ANALYSIS:' in result
2171 | assert 'Memory: 256.8 MB peak' in result
2172 |
2173 |
2174 | @pytest.mark.asyncio
2175 | async def test_analyze_canary_failures_navigation_timeout_with_har(mock_aws_clients):
2176 | """Test analyze_canary_failures with navigation timeout and HAR analysis."""
2177 | from awslabs.cloudwatch_appsignals_mcp_server.server import analyze_canary_failures
2178 |
2179 | mock_runs = [
2180 | {
2181 | 'Id': 'failed-run',
2182 | 'Status': {'State': 'FAILED', 'StateReason': 'Navigation timed out after 30000ms'},
2183 | 'Timeline': {'Started': '2024-01-01T00:00:00Z'},
2184 | }
2185 | ]
2186 |
2187 | mock_canary = {
2188 | 'Name': 'test-canary',
2189 | 'ArtifactS3Location': 's3://test-bucket/canary/us-east-1/test-canary',
2190 | }
2191 |
2192 | mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
2193 | mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}
2194 |
2195 | # Mock S3 to return HAR files
2196 | mock_aws_clients['s3_client'].list_objects_v2.return_value = {
2197 | 'Contents': [
2198 | {'Key': 'canary/us-east-1/test-canary/2024/01/01/test.har'},
2199 | ]
2200 | }
2201 |
2202 | with patch(
2203 | 'awslabs.cloudwatch_appsignals_mcp_server.server.get_canary_metrics_and_service_insights'
2204 | ) as mock_insights:
2205 | with patch('awslabs.cloudwatch_appsignals_mcp_server.server.analyze_har_file') as mock_har:
2206 | mock_insights.return_value = 'Telemetry insights'
2207 | mock_har.return_value = {
2208 | 'failed_requests': 5,
2209 | 'total_requests': 10,
2210 | 'request_details': [
2211 | {'url': 'https://example.com/slow', 'status': 200, 'time': 5000}
2212 | ],
2213 | 'insights': [
2214 | 'Slow DNS resolution detected',
2215 | 'High server response time',
2216 | 'Network connectivity issues',
2217 | 'Resource loading delays',
2218 | 'JavaScript execution timeout',
2219 | ],
2220 | }
2221 |
2222 | result = await analyze_canary_failures('test-canary', 'us-east-1')
2223 |
2224 | assert '🔍 Comprehensive Failure Analysis for test-canary' in result
2225 | assert 'Slow DNS resolution detected' in result
2226 |
2227 |
2228 | @pytest.mark.asyncio
2229 | async def test_analyze_canary_failures_s3_exception(mock_aws_clients):
2230 | """Test analyze_canary_failures when S3 operations fail."""
2231 | from awslabs.cloudwatch_appsignals_mcp_server.server import analyze_canary_failures
2232 |
2233 | mock_runs = [
2234 | {
2235 | 'Id': 'failed-run',
2236 | 'Status': {'State': 'FAILED', 'StateReason': 'Test failure'},
2237 | 'Timeline': {'Started': '2024-01-01T00:00:00Z'},
2238 | }
2239 | ]
2240 |
2241 | mock_canary = {
2242 | 'Name': 'test-canary',
2243 | 'ArtifactS3Location': 's3://test-bucket/canary/us-east-1/test-canary',
2244 | }
2245 |
2246 | mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
2247 | mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}
2248 |
2249 | mock_aws_clients['s3_client'].list_objects_v2.side_effect = Exception('S3 access denied')
2250 |
2251 | with patch(
2252 | 'awslabs.cloudwatch_appsignals_mcp_server.server.get_canary_metrics_and_service_insights'
2253 | ) as mock_insights:
2254 | with patch(
2255 | 'awslabs.cloudwatch_appsignals_mcp_server.server.analyze_canary_logs_with_time_window'
2256 | ) as mock_logs:
2257 | mock_insights.return_value = 'Telemetry insights'
2258 | mock_logs.return_value = {
2259 | 'status': 'success',
2260 | 'time_window': '2024-01-01 00:00:00 - 2024-01-01 00:05:00',
2261 | 'total_events': 5,
2262 | 'error_events': [
2263 | {'timestamp': datetime.now(timezone.utc), 'message': 'Test error message'}
2264 | ],
2265 | }
2266 |
2267 | result = await analyze_canary_failures('test-canary', 'us-east-1')
2268 |
2269 | # Should fall back to CloudWatch Logs analysis when S3 fails
2270 | assert '⚠️ Artifacts not available - Checking CloudWatch Logs for root cause' in result
2271 | assert 'CLOUDWATCH LOGS ANALYSIS' in result
2272 |
2273 |
2274 | @pytest.mark.asyncio
2275 | async def test_analyze_canary_failures_visual_variation(mock_aws_clients):
2276 | """Test analyze_canary_failures with visual variation error."""
2277 | from awslabs.cloudwatch_appsignals_mcp_server.server import analyze_canary_failures
2278 |
2279 | mock_runs = [
2280 | {
2281 | 'Id': 'failed-run',
2282 | 'Status': {'State': 'FAILED', 'StateReason': 'Visual variation detected'},
2283 | 'Timeline': {'Started': '2024-01-01T00:00:00Z'},
2284 | }
2285 | ]
2286 |
2287 | mock_canary = {'Name': 'test-canary', 'ArtifactS3Location': ''}
2288 |
2289 | mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
2290 | mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}
2291 |
2292 | with patch(
2293 | 'awslabs.cloudwatch_appsignals_mcp_server.server.get_canary_metrics_and_service_insights'
2294 | ) as mock_insights:
2295 | with patch('awslabs.cloudwatch_appsignals_mcp_server.server.get_canary_code') as mock_code:
2296 | mock_insights.return_value = 'Telemetry insights'
2297 | mock_code.return_value = {'code_content': 'const synthetics = require("Synthetics");'}
2298 |
2299 | result = await analyze_canary_failures('test-canary', 'us-east-1')
2300 |
2301 | assert 'VISUAL MONITORING ISSUE DETECTED' in result
2302 | assert 'Website UI changed - not a technical failure' in result
2303 | assert 'canary code:' in result
2304 |
2305 |
2306 | @pytest.mark.asyncio
2307 | async def test_analyze_canary_failures_get_canary_code_exception(mock_aws_clients):
2308 | """Test analyze_canary_failures when get_canary_code fails."""
2309 | from awslabs.cloudwatch_appsignals_mcp_server.server import analyze_canary_failures
2310 |
2311 | mock_runs = [
2312 | {
2313 | 'Id': 'failed-run',
2314 | 'Status': {'State': 'FAILED', 'StateReason': 'Test failure'},
2315 | 'Timeline': {'Started': '2024-01-01T00:00:00Z'},
2316 | }
2317 | ]
2318 |
2319 | mock_canary = {'Name': 'test-canary', 'ArtifactS3Location': ''}
2320 |
2321 | mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
2322 | mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}
2323 |
2324 | with patch(
2325 | 'awslabs.cloudwatch_appsignals_mcp_server.server.get_canary_metrics_and_service_insights'
2326 | ) as mock_insights:
2327 | with patch('awslabs.cloudwatch_appsignals_mcp_server.server.get_canary_code') as mock_code:
2328 | mock_insights.return_value = 'Telemetry insights'
2329 | # Make get_canary_code raise an exception
2330 | mock_code.side_effect = Exception('Code retrieval failed')
2331 |
2332 | result = await analyze_canary_failures('test-canary', 'us-east-1')
2333 |
2334 | assert 'Note: Could not retrieve canary code: Code retrieval failed' in result
2335 |
2336 |
2337 | @pytest.mark.asyncio
2338 | async def test_analyze_canary_failures_iam_analysis_exception(mock_aws_clients):
2339 | """Test analyze_canary_failures when IAM analysis fails."""
2340 | from awslabs.cloudwatch_appsignals_mcp_server.server import analyze_canary_failures
2341 |
2342 | mock_runs = [
2343 | {
2344 | 'Id': 'failed-run',
2345 | 'Status': {'State': 'FAILED', 'StateReason': 'Access denied'},
2346 | 'Timeline': {'Started': '2024-01-01T00:00:00Z'},
2347 | }
2348 | ]
2349 |
2350 | mock_canary = {'Name': 'test-canary', 'ArtifactS3Location': ''}
2351 |
2352 | mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
2353 | mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}
2354 |
2355 | with patch(
2356 | 'awslabs.cloudwatch_appsignals_mcp_server.server.get_canary_metrics_and_service_insights'
2357 | ) as mock_insights:
2358 | with patch(
2359 | 'awslabs.cloudwatch_appsignals_mcp_server.server.analyze_iam_role_and_policies'
2360 | ) as mock_iam:
2361 | mock_insights.return_value = 'Telemetry insights'
2362 | # Make IAM analysis raise an exception
2363 | mock_iam.side_effect = Exception('IAM analysis failed')
2364 |
2365 | result = await analyze_canary_failures('test-canary', 'us-east-1')
2366 |
2367 | assert '⚠️ IAM analysis failed: IAM analysis failed' in result
2368 |
2369 |
2370 | @pytest.mark.asyncio
2371 | async def test_analyze_canary_failures_disk_usage_exception(mock_aws_clients):
2372 | """Test analyze_canary_failures when disk usage analysis fails."""
2373 | from awslabs.cloudwatch_appsignals_mcp_server.server import analyze_canary_failures
2374 |
2375 | mock_runs = [
2376 | {
2377 | 'Id': 'failed-run',
2378 | 'Status': {'State': 'FAILED', 'StateReason': 'ENOSPC: no space left on device'},
2379 | 'Timeline': {'Started': '2024-01-01T00:00:00Z'},
2380 | }
2381 | ]
2382 |
2383 | mock_canary = {'Name': 'test-canary', 'ArtifactS3Location': ''}
2384 |
2385 | mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
2386 | mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}
2387 |
2388 | with patch(
2389 | 'awslabs.cloudwatch_appsignals_mcp_server.server.get_canary_metrics_and_service_insights'
2390 | ) as mock_insights:
2391 | with patch(
2392 | 'awslabs.cloudwatch_appsignals_mcp_server.server.extract_disk_memory_usage_metrics'
2393 | ) as mock_metrics:
2394 | mock_insights.return_value = 'Telemetry insights'
2395 | # Make disk usage analysis raise an exception
2396 | mock_metrics.side_effect = Exception('Disk usage analysis failed')
2397 |
2398 | result = await analyze_canary_failures('test-canary', 'us-east-1')
2399 |
2400 | assert (
2401 | '⚠️ Could not generate disk usage debugging code: Disk usage analysis failed'
2402 | in result
2403 | )
2404 |
2405 |
2406 | @pytest.mark.asyncio
2407 | async def test_analyze_canary_failures_memory_usage_exception(mock_aws_clients):
2408 | """Test analyze_canary_failures when memory usage analysis fails."""
2409 | from awslabs.cloudwatch_appsignals_mcp_server.server import analyze_canary_failures
2410 |
2411 | mock_runs = [
2412 | {
2413 | 'Id': 'failed-run',
2414 | 'Status': {
2415 | 'State': 'FAILED',
2416 | 'StateReason': 'Protocol error (Target.activateTarget): Session closed',
2417 | },
2418 | 'Timeline': {'Started': '2024-01-01T00:00:00Z'},
2419 | }
2420 | ]
2421 |
2422 | mock_canary = {'Name': 'test-canary', 'ArtifactS3Location': ''}
2423 |
2424 | mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
2425 | mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}
2426 |
2427 | with patch(
2428 | 'awslabs.cloudwatch_appsignals_mcp_server.server.get_canary_metrics_and_service_insights'
2429 | ) as mock_insights:
2430 | with patch(
2431 | 'awslabs.cloudwatch_appsignals_mcp_server.server.extract_disk_memory_usage_metrics'
2432 | ) as mock_metrics:
2433 | mock_insights.return_value = 'Telemetry insights'
2434 | # Make memory usage analysis raise an exception
2435 | mock_metrics.side_effect = Exception('Memory usage analysis failed')
2436 |
2437 | result = await analyze_canary_failures('test-canary', 'us-east-1')
2438 |
2439 | assert (
2440 | '⚠️ Could not collect memory usage metrics: Memory usage analysis failed' in result
2441 | )
2442 |
2443 |
2444 | @pytest.mark.asyncio
2445 | async def test_analyze_canary_failures_har_timeout_exception(mock_aws_clients):
2446 | """Test analyze_canary_failures when HAR timeout analysis fails."""
2447 | from awslabs.cloudwatch_appsignals_mcp_server.server import analyze_canary_failures
2448 |
2449 | mock_runs = [
2450 | {
2451 | 'Id': 'failed-run',
2452 | 'Status': {'State': 'FAILED', 'StateReason': 'Navigation timed out after 30000ms'},
2453 | 'Timeline': {'Started': '2024-01-01T00:00:00Z'},
2454 | }
2455 | ]
2456 |
2457 | mock_canary = {
2458 | 'Name': 'test-canary',
2459 | 'ArtifactS3Location': 's3://test-bucket/canary/us-east-1/test-canary',
2460 | }
2461 |
2462 | mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
2463 | mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}
2464 |
2465 | # Mock S3 to return HAR files
2466 | mock_aws_clients['s3_client'].list_objects_v2.return_value = {
2467 | 'Contents': [
2468 | {'Key': 'canary/us-east-1/test-canary/2024/01/01/test.har'},
2469 | ]
2470 | }
2471 |
2472 | with patch(
2473 | 'awslabs.cloudwatch_appsignals_mcp_server.server.get_canary_metrics_and_service_insights'
2474 | ) as mock_insights:
2475 | with patch('awslabs.cloudwatch_appsignals_mcp_server.server.analyze_har_file') as mock_har:
2476 | mock_insights.return_value = 'Telemetry insights'
2477 | # Make HAR analysis raise an exception
2478 | mock_har.side_effect = Exception('HAR analysis failed')
2479 |
2480 | result = await analyze_canary_failures('test-canary', 'us-east-1')
2481 |
2482 | assert '⚠️ HAR analysis failed: HAR analysis failed' in result
2483 |
2484 |
2485 | @pytest.mark.asyncio
2486 | async def test_analyze_canary_failures_success_artifacts_exception(mock_aws_clients):
2487 | """Test analyze_canary_failures when success artifacts retrieval fails."""
2488 | from awslabs.cloudwatch_appsignals_mcp_server.server import analyze_canary_failures
2489 |
2490 | mock_runs = [
2491 | {
2492 | 'Id': 'failed-run',
2493 | 'Status': {'State': 'FAILED', 'StateReason': 'Test failure'},
2494 | 'Timeline': {'Started': '2024-01-01T00:00:00Z'},
2495 | },
2496 | {
2497 | 'Id': 'success-run',
2498 | 'Status': {'State': 'PASSED'},
2499 | 'Timeline': {'Started': '2024-01-01T00:00:00Z'},
2500 | },
2501 | ]
2502 |
2503 | mock_canary = {
2504 | 'Name': 'test-canary',
2505 | 'ArtifactS3Location': 's3://test-bucket/canary/us-east-1/test-canary',
2506 | }
2507 |
2508 | mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
2509 | mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}
2510 |
2511 | # Mock S3 to return failure artifacts but fail on success artifacts
2512 | def s3_side_effect(*args, **kwargs):
2513 | prefix = kwargs.get('Prefix', '')
2514 | if 'success' in prefix or len(prefix.split('/')) > 5: # Simulate success path failure
2515 | raise Exception('Success artifacts access failed')
2516 | return {
2517 | 'Contents': [
2518 | {'Key': 'canary/us-east-1/test-canary/2024/01/01/test.har'},
2519 | ]
2520 | }
2521 |
2522 | mock_aws_clients['s3_client'].list_objects_v2.side_effect = s3_side_effect
2523 |
2524 | with patch(
2525 | 'awslabs.cloudwatch_appsignals_mcp_server.server.get_canary_metrics_and_service_insights'
2526 | ) as mock_insights:
2527 | with patch('awslabs.cloudwatch_appsignals_mcp_server.server.analyze_har_file') as mock_har:
2528 | mock_insights.return_value = 'Telemetry insights'
2529 | mock_har.return_value = {
2530 | 'failed_requests': 2,
2531 | 'total_requests': 10,
2532 | 'request_details': [],
2533 | }
2534 |
2535 | result = await analyze_canary_failures('test-canary', 'us-east-1')
2536 |
2537 | # Should still process failure artifacts even if success artifacts fail
2538 | assert '🔍 Comprehensive Failure Analysis for test-canary' in result
2539 |
2540 |
2541 | @pytest.mark.asyncio
2542 | async def test_analyze_canary_failures_no_failure_timestamp(mock_aws_clients):
2543 | """Test analyze_canary_failures when failure has no timestamp."""
2544 | from awslabs.cloudwatch_appsignals_mcp_server.server import analyze_canary_failures
2545 |
2546 | mock_runs = [
2547 | {
2548 | 'Id': 'failed-run',
2549 | 'Status': {'State': 'FAILED', 'StateReason': 'Test failure'},
2550 | 'Timeline': {}, # No Started timestamp
2551 | }
2552 | ]
2553 |
2554 | mock_canary = {'Name': 'test-canary', 'ArtifactS3Location': ''}
2555 |
2556 | mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
2557 | mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}
2558 |
2559 | with patch(
2560 | 'awslabs.cloudwatch_appsignals_mcp_server.server.get_canary_metrics_and_service_insights'
2561 | ) as mock_insights:
2562 | mock_insights.return_value = 'Telemetry insights'
2563 |
2564 | result = await analyze_canary_failures('test-canary', 'us-east-1')
2565 |
2566 | assert '📋 No failure timestamp available for targeted log analysis' in result
2567 |
2568 |
2569 | @pytest.mark.asyncio
2570 | async def test_analyze_canary_failures_log_analysis_failure(mock_aws_clients):
2571 | """Test analyze_canary_failures when log analysis fails."""
2572 | from awslabs.cloudwatch_appsignals_mcp_server.server import analyze_canary_failures
2573 |
2574 | mock_runs = [
2575 | {
2576 | 'Id': 'failed-run',
2577 | 'Status': {'State': 'FAILED', 'StateReason': 'Test failure'},
2578 | 'Timeline': {'Started': '2024-01-01T00:00:00Z'},
2579 | }
2580 | ]
2581 |
2582 | mock_canary = {'Name': 'test-canary', 'ArtifactS3Location': ''}
2583 |
2584 | mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
2585 | mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}
2586 |
2587 | with patch(
2588 | 'awslabs.cloudwatch_appsignals_mcp_server.server.get_canary_metrics_and_service_insights'
2589 | ) as mock_insights:
2590 | with patch(
2591 | 'awslabs.cloudwatch_appsignals_mcp_server.server.analyze_canary_logs_with_time_window'
2592 | ) as mock_logs:
2593 | mock_insights.return_value = 'Telemetry insights'
2594 | mock_logs.return_value = {
2595 | 'status': 'failed',
2596 | 'insights': ['Log analysis failed due to missing log group'],
2597 | }
2598 |
2599 | result = await analyze_canary_failures('test-canary', 'us-east-1')
2600 |
2601 | assert '📋 Log analysis failed due to missing log group' in result
2602 |
2603 |
2604 | @pytest.mark.asyncio
2605 | async def test_analyze_canary_failures_main_exception(mock_aws_clients):
2606 | """Test analyze_canary_failures when main function fails."""
2607 | from awslabs.cloudwatch_appsignals_mcp_server.server import analyze_canary_failures
2608 |
2609 | # Make get_canary_runs raise an exception
2610 | mock_aws_clients['synthetics_client'].get_canary_runs.side_effect = Exception('API error')
2611 |
2612 | result = await analyze_canary_failures('test-canary', 'us-east-1')
2613 |
2614 | assert '❌ Error in comprehensive failure analysis: API error' in result
2615 |
2616 |
2617 | @pytest.mark.asyncio
2618 | async def test_analyze_canary_failures_no_har_files_navigation_timeout(mock_aws_clients):
2619 | """Test analyze_canary_failures navigation timeout without HAR files."""
2620 | from awslabs.cloudwatch_appsignals_mcp_server.server import analyze_canary_failures
2621 |
2622 | mock_runs = [
2623 | {
2624 | 'Id': 'failed-run',
2625 | 'Status': {'State': 'FAILED', 'StateReason': 'Navigation timed out after 30000ms'},
2626 | 'Timeline': {'Started': '2024-01-01T00:00:00Z'},
2627 | }
2628 | ]
2629 |
2630 | mock_canary = {'Name': 'test-canary', 'ArtifactS3Location': ''}
2631 |
2632 | mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
2633 | mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}
2634 |
2635 | with patch(
2636 | 'awslabs.cloudwatch_appsignals_mcp_server.server.get_canary_metrics_and_service_insights'
2637 | ) as mock_insights:
2638 | mock_insights.return_value = 'Telemetry insights'
2639 |
2640 | result = await analyze_canary_failures('test-canary', 'us-east-1')
2641 |
2642 | assert 'NAVIGATION TIMEOUT DETECTED:' in result
2643 | assert 'No HAR files available for detailed analysis' in result
2644 |
2645 |
2646 | @pytest.mark.asyncio
2647 | async def test_analyze_canary_failures_artifact_location_without_s3_prefix(mock_aws_clients):
2648 | """Test analyze_canary_failures with artifact location without s3:// prefix."""
2649 | from awslabs.cloudwatch_appsignals_mcp_server.server import analyze_canary_failures
2650 |
2651 | mock_runs = [
2652 | {
2653 | 'Id': 'failed-run',
2654 | 'Status': {'State': 'FAILED', 'StateReason': 'Test failure'},
2655 | 'Timeline': {'Started': '2024-01-01T00:00:00Z'},
2656 | }
2657 | ]
2658 |
2659 | mock_canary = {
2660 | 'Name': 'test-canary',
2661 | 'ArtifactS3Location': 'test-bucket/canary/us-east-1/test-canary', # No s3:// prefix
2662 | }
2663 |
2664 | mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
2665 | mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}
2666 |
2667 | # Mock S3 to return artifacts
2668 | mock_aws_clients['s3_client'].list_objects_v2.return_value = {
2669 | 'Contents': [
2670 | {'Key': 'canary/us-east-1/test-canary/2024/01/01/test.har'},
2671 | ]
2672 | }
2673 |
2674 | with patch(
2675 | 'awslabs.cloudwatch_appsignals_mcp_server.server.get_canary_metrics_and_service_insights'
2676 | ) as mock_insights:
2677 | with patch('awslabs.cloudwatch_appsignals_mcp_server.server.analyze_har_file') as mock_har:
2678 | mock_insights.return_value = 'Telemetry insights'
2679 | mock_har.return_value = {
2680 | 'failed_requests': 1,
2681 | 'total_requests': 5,
2682 | 'request_details': [],
2683 | }
2684 |
2685 | result = await analyze_canary_failures('test-canary', 'us-east-1')
2686 |
2687 | # Should still process artifacts even without s3:// prefix
2688 | assert 'FAILURE ANALYSIS' in result
2689 |
2690 |
2691 | @pytest.mark.asyncio
2692 | async def test_analyze_canary_failures_empty_base_path(mock_aws_clients):
2693 | """Test analyze_canary_failures with empty base path."""
2694 | from awslabs.cloudwatch_appsignals_mcp_server.server import analyze_canary_failures
2695 |
2696 | mock_runs = [
2697 | {
2698 | 'Id': 'failed-run',
2699 | 'Status': {'State': 'FAILED', 'StateReason': 'Test failure'},
2700 | 'Timeline': {'Started': '2024-01-01T00:00:00Z'},
2701 | }
2702 | ]
2703 |
2704 | mock_canary = {
2705 | 'Name': 'test-canary',
2706 | 'ArtifactS3Location': 's3://test-bucket', # Only bucket, no path
2707 | }
2708 |
2709 | mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
2710 | mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}
2711 |
2712 | # Mock S3 to return artifacts
2713 | mock_aws_clients['s3_client'].list_objects_v2.return_value = {
2714 | 'Contents': [
2715 | {'Key': 'canary/us-east-1/test-canary/2024/01/01/test.har'},
2716 | ]
2717 | }
2718 |
2719 | with patch(
2720 | 'awslabs.cloudwatch_appsignals_mcp_server.server.get_canary_metrics_and_service_insights'
2721 | ) as mock_insights:
2722 | with patch('awslabs.cloudwatch_appsignals_mcp_server.server.analyze_har_file') as mock_har:
2723 | mock_insights.return_value = 'Telemetry insights'
2724 | mock_har.return_value = {
2725 | 'failed_requests': 1,
2726 | 'total_requests': 5,
2727 | 'request_details': [],
2728 | }
2729 |
2730 | result = await analyze_canary_failures('test-canary', 'us-east-1')
2731 |
2732 | # Should construct default canary path when base_path is empty
2733 | assert 'FAILURE ANALYSIS' in result
2734 |
2735 |
2736 | @pytest.mark.asyncio
2737 | async def test_analyze_canary_failures_multiple_failure_causes(mock_aws_clients):
2738 | """Test analyze_canary_failures with multiple different failure causes."""
2739 | from awslabs.cloudwatch_appsignals_mcp_server.server import analyze_canary_failures
2740 |
2741 | mock_runs = [
2742 | {
2743 | 'Id': 'failed-run-1',
2744 | 'Status': {'State': 'FAILED', 'StateReason': 'Navigation timeout'},
2745 | 'Timeline': {'Started': '2024-01-01T00:00:00Z'},
2746 | },
2747 | {
2748 | 'Id': 'failed-run-2',
2749 | 'Status': {'State': 'FAILED', 'StateReason': 'Access denied'},
2750 | 'Timeline': {'Started': '2024-01-01T00:01:00Z'},
2751 | },
2752 | {
2753 | 'Id': 'success-run',
2754 | 'Status': {'State': 'PASSED'},
2755 | 'Timeline': {'Started': '2023-12-31T23:59:00Z'},
2756 | },
2757 | ]
2758 |
2759 | mock_canary = {'Name': 'test-canary', 'ArtifactS3Location': ''}
2760 |
2761 | mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
2762 | mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}
2763 |
2764 | with patch(
2765 | 'awslabs.cloudwatch_appsignals_mcp_server.server.get_canary_metrics_and_service_insights'
2766 | ) as mock_insights:
2767 | mock_insights.return_value = 'Telemetry insights'
2768 |
2769 | result = await analyze_canary_failures('test-canary', 'us-east-1')
2770 |
2771 | assert 'Multiple failure causes (2 different issues):' in result
2772 | assert '1. **Navigation timeout** (1 occurrences)' in result
2773 | assert '2. **Access denied** (1 occurrences)' in result
2774 |
2775 |
2776 | @pytest.mark.asyncio
2777 | async def test_analyze_canary_failures_no_failure_time_fallback(mock_aws_clients):
2778 | """Test analyze_canary_failures fallback when no failure time."""
2779 | from awslabs.cloudwatch_appsignals_mcp_server.server import analyze_canary_failures
2780 |
2781 | mock_runs = [
2782 | {
2783 | 'Id': 'failed-run',
2784 | 'Status': {'State': 'FAILED', 'StateReason': 'Test failure'},
2785 | 'Timeline': {}, # No Started time
2786 | }
2787 | ]
2788 |
2789 | mock_canary = {
2790 | 'Name': 'test-canary',
2791 | 'ArtifactS3Location': 's3://test-bucket/canary/us-east-1/test-canary',
2792 | }
2793 |
2794 | mock_aws_clients['synthetics_client'].get_canary_runs.return_value = {'CanaryRuns': mock_runs}
2795 | mock_aws_clients['synthetics_client'].get_canary.return_value = {'Canary': mock_canary}
2796 |
2797 | # Mock S3 to return artifacts
2798 | mock_aws_clients['s3_client'].list_objects_v2.return_value = {
2799 | 'Contents': [
2800 | {'Key': 'canary/us-east-1/test-canary/2024/01/01/test.har'},
2801 | ]
2802 | }
2803 |
2804 | with patch(
2805 | 'awslabs.cloudwatch_appsignals_mcp_server.server.get_canary_metrics_and_service_insights'
2806 | ) as mock_insights:
2807 | with patch('awslabs.cloudwatch_appsignals_mcp_server.server.analyze_har_file') as mock_har:
2808 | mock_insights.return_value = 'Telemetry insights'
2809 | mock_har.return_value = {
2810 | 'failed_requests': 1,
2811 | 'total_requests': 5,
2812 | 'request_details': [],
2813 | }
2814 |
2815 | result = await analyze_canary_failures('test-canary', 'us-east-1')
2816 |
2817 | # Should use current time when no failure time available
2818 | assert 'FAILURE ANALYSIS' in result
2819 |
2820 |
2821 | def test_filter_operation_targets_fault_to_availability():
2822 | """Test _filter_operation_targets converts Fault to Availability."""
2823 | provided = [
2824 | {
2825 | 'Type': 'service_operation',
2826 | 'Data': {
2827 | 'ServiceOperation': {
2828 | 'Service': {'Type': 'Service', 'Name': 'test-service'},
2829 | 'Operation': 'GET /api',
2830 | 'MetricType': 'Fault',
2831 | }
2832 | },
2833 | }
2834 | ]
2835 |
2836 | operation_targets, has_wildcards = _filter_operation_targets(provided)
2837 |
2838 | # Verify the MetricType was changed from Fault to Availability
2839 | assert len(operation_targets) == 1
2840 | assert operation_targets[0]['Data']['ServiceOperation']['MetricType'] == 'Availability'
2841 | assert has_wildcards is False
2842 |
2843 |
2844 | def test_filter_operation_targets_non_fault_unchanged():
2845 | """Test _filter_operation_targets leaves non-Fault MetricTypes unchanged."""
2846 | provided = [
2847 | {
2848 | 'Type': 'service_operation',
2849 | 'Data': {
2850 | 'ServiceOperation': {
2851 | 'Service': {'Type': 'Service', 'Name': 'test-service'},
2852 | 'Operation': 'GET /api',
2853 | 'MetricType': 'Latency',
2854 | }
2855 | },
2856 | },
2857 | {
2858 | 'Type': 'service_operation',
2859 | 'Data': {
2860 | 'ServiceOperation': {
2861 | 'Service': {'Type': 'Service', 'Name': 'test-service-2'},
2862 | 'Operation': 'POST /api',
2863 | 'MetricType': 'Error',
2864 | }
2865 | },
2866 | },
2867 | ]
2868 |
2869 | operation_targets, has_wildcards = _filter_operation_targets(provided)
2870 |
2871 | # Verify non-Fault MetricTypes are unchanged
2872 | assert len(operation_targets) == 2
2873 | assert operation_targets[0]['Data']['ServiceOperation']['MetricType'] == 'Latency'
2874 | assert operation_targets[1]['Data']['ServiceOperation']['MetricType'] == 'Error'
2875 | assert has_wildcards is False
2876 |
2877 |
2878 | def test_filter_operation_targets_multiple_fault_conversions():
2879 | """Test _filter_operation_targets converts multiple Fault entries to Availability."""
2880 | provided = [
2881 | {
2882 | 'Type': 'service_operation',
2883 | 'Data': {
2884 | 'ServiceOperation': {
2885 | 'Service': {'Type': 'Service', 'Name': 'service-1'},
2886 | 'Operation': 'GET /api',
2887 | 'MetricType': 'Fault',
2888 | }
2889 | },
2890 | },
2891 | {
2892 | 'Type': 'service_operation',
2893 | 'Data': {
2894 | 'ServiceOperation': {
2895 | 'Service': {'Type': 'Service', 'Name': 'service-2'},
2896 | 'Operation': 'POST /api',
2897 | 'MetricType': 'Latency',
2898 | }
2899 | },
2900 | },
2901 | {
2902 | 'Type': 'service_operation',
2903 | 'Data': {
2904 | 'ServiceOperation': {
2905 | 'Service': {'Type': 'Service', 'Name': 'service-3'},
2906 | 'Operation': 'PUT /api',
2907 | 'MetricType': 'Fault',
2908 | }
2909 | },
2910 | },
2911 | ]
2912 |
2913 | operation_targets, has_wildcards = _filter_operation_targets(provided)
2914 |
2915 | # Verify multiple Fault entries are converted
2916 | assert len(operation_targets) == 3
2917 | assert operation_targets[0]['Data']['ServiceOperation']['MetricType'] == 'Availability'
2918 | assert operation_targets[1]['Data']['ServiceOperation']['MetricType'] == 'Latency'
2919 | assert operation_targets[2]['Data']['ServiceOperation']['MetricType'] == 'Availability'
2920 | assert has_wildcards is False
2921 |
2922 |
2923 | def test_filter_operation_targets_with_wildcards():
2924 | """Test _filter_operation_targets detects wildcards and converts Fault to Availability."""
2925 | provided = [
2926 | {
2927 | 'Type': 'service_operation',
2928 | 'Data': {
2929 | 'ServiceOperation': {
2930 | 'Service': {'Type': 'Service', 'Name': '*payment*'},
2931 | 'Operation': '*GET*',
2932 | 'MetricType': 'Fault',
2933 | }
2934 | },
2935 | }
2936 | ]
2937 |
2938 | operation_targets, has_wildcards = _filter_operation_targets(provided)
2939 |
2940 | # Verify wildcard detection and Fault conversion
2941 | assert len(operation_targets) == 1
2942 | assert operation_targets[0]['Data']['ServiceOperation']['MetricType'] == 'Availability'
2943 | assert has_wildcards is True
2944 |
2945 |
2946 | def test_filter_operation_targets_ignores_non_service_operation():
2947 | """Test _filter_operation_targets ignores non-service_operation targets."""
2948 | provided = [
2949 | {
2950 | 'Type': 'service',
2951 | 'Data': {'Service': {'Type': 'Service', 'Name': 'test-service'}},
2952 | },
2953 | {
2954 | 'Type': 'service_operation',
2955 | 'Data': {
2956 | 'ServiceOperation': {
2957 | 'Service': {'Type': 'Service', 'Name': 'test-service'},
2958 | 'Operation': 'GET /api',
2959 | 'MetricType': 'Fault',
2960 | }
2961 | },
2962 | },
2963 | ]
2964 |
2965 | operation_targets, has_wildcards = _filter_operation_targets(provided)
2966 |
2967 | # Verify only service_operation targets are included
2968 | assert len(operation_targets) == 1
2969 | assert operation_targets[0]['Type'] == 'service_operation'
2970 | assert operation_targets[0]['Data']['ServiceOperation']['MetricType'] == 'Availability'
2971 | assert has_wildcards is False
2972 |
2973 |
2974 | def test_filter_operation_targets_empty_metric_type():
2975 | """Test _filter_operation_targets handles empty MetricType gracefully."""
2976 | provided = [
2977 | {
2978 | 'Type': 'service_operation',
2979 | 'Data': {
2980 | 'ServiceOperation': {
2981 | 'Service': {'Type': 'Service', 'Name': 'test-service'},
2982 | 'Operation': 'GET /api',
2983 | 'MetricType': '',
2984 | }
2985 | },
2986 | }
2987 | ]
2988 |
2989 | operation_targets, has_wildcards = _filter_operation_targets(provided)
2990 |
2991 | # Verify empty MetricType is unchanged
2992 | assert len(operation_targets) == 1
2993 | assert operation_targets[0]['Data']['ServiceOperation']['MetricType'] == ''
2994 | assert has_wildcards is False
2995 |
2996 |
2997 | def test_filter_operation_targets_missing_metric_type():
2998 | """Test _filter_operation_targets handles missing MetricType gracefully."""
2999 | provided = [
3000 | {
3001 | 'Type': 'service_operation',
3002 | 'Data': {
3003 | 'ServiceOperation': {
3004 | 'Service': {'Type': 'Service', 'Name': 'test-service'},
3005 | 'Operation': 'GET /api',
3006 | # MetricType is missing
3007 | }
3008 | },
3009 | }
3010 | ]
3011 |
3012 | operation_targets, has_wildcards = _filter_operation_targets(provided)
3013 |
3014 | # Verify missing MetricType doesn't cause errors
3015 | assert len(operation_targets) == 1
3016 | # MetricType should remain missing (empty string from .get())
3017 | assert operation_targets[0]['Data']['ServiceOperation'].get('MetricType', '') == ''
3018 | assert has_wildcards is False
3019 |
3020 |
3021 | def test_filter_operation_targets_case_sensitive():
3022 | """Test _filter_operation_targets is case-sensitive for Fault conversion."""
3023 | provided = [
3024 | {
3025 | 'Type': 'service_operation',
3026 | 'Data': {
3027 | 'ServiceOperation': {
3028 | 'Service': {'Type': 'Service', 'Name': 'test-service'},
3029 | 'Operation': 'GET /api',
3030 | 'MetricType': 'fault', # lowercase
3031 | }
3032 | },
3033 | },
3034 | {
3035 | 'Type': 'service_operation',
3036 | 'Data': {
3037 | 'ServiceOperation': {
3038 | 'Service': {'Type': 'Service', 'Name': 'test-service-2'},
3039 | 'Operation': 'POST /api',
3040 | 'MetricType': 'FAULT', # uppercase
3041 | }
3042 | },
3043 | },
3044 | ]
3045 |
3046 | operation_targets, has_wildcards = _filter_operation_targets(provided)
3047 |
3048 | # Verify only exact case "Fault" is converted
3049 | assert len(operation_targets) == 2
3050 | assert operation_targets[0]['Data']['ServiceOperation']['MetricType'] == 'fault' # unchanged
3051 | assert operation_targets[1]['Data']['ServiceOperation']['MetricType'] == 'FAULT' # unchanged
3052 | assert has_wildcards is False
3053 |
```