Skip to content

Commit 953e07a

Browse files
Merge branch 'main' into chore/update-fastmcp
2 parents 0e1cec1 + c64076b commit 953e07a

File tree

3 files changed

+366
-0
lines changed

3 files changed

+366
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Placeholder content just to verify the tool can fetch the file.
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""CloudWatch Application Signals MCP Server - Enablement Tools."""
16+
17+
from loguru import logger
18+
from pathlib import Path
19+
20+
21+
async def get_enablement_guide(
22+
service_platform: str,
23+
service_language: str,
24+
iac_directory: str,
25+
app_directory: str,
26+
) -> str:
27+
"""Get enablement guide for AWS Application Signals.
28+
29+
Use this tool when the user wants to:
30+
- Enable Application Signals for their AWS service
31+
- Set up automatic instrumentation for their application on AWS
32+
- Instrument their service running on EC2, ECS, Lambda, or EKS
33+
34+
This tool returns step-by-step enablement instructions that guide you through
35+
modifying your infrastructure and application code to enable Application Signals,
36+
which is the preferred way to enable automatic instrumentation for services on AWS.
37+
38+
Before calling this tool:
39+
1. Ensure you know where the application code is located and that you have read/write permissions
40+
2. Ensure you know where the IaC code is located and that you have read/write permissions
41+
3. If the user provides relative paths or descriptions (e.g., "./infrastructure", "in the root"):
42+
- Use the Bash tool to run 'pwd' to get the current working directory
43+
- Use file exploration tools to locate the directories
44+
- Convert relative paths to absolute paths before calling this tool
45+
4. This tool REQUIRES absolute paths for both iac_directory and app_directory parameters
46+
47+
After calling this tool, you should:
48+
1. Review the enablement guide and create a visible, trackable checklist of required changes
49+
- Use your system's task tracking mechanism (todo lists, markdown checklists, etc.)
50+
- Each item should be granular enough to complete in one step
51+
- Mark items as complete as you finish them to track progress
52+
- This allows you to resume work if the context window fills up
53+
2. Work through the checklist systematically, one item at a time:
54+
- Identify the specific file(s) that need modification for this step
55+
- Read only the relevant file(s) (DO NOT load all IaC and app files at once)
56+
- Apply the changes as specified in the guide
57+
3. Keep context focused: Only load files needed for the current checklist item
58+
59+
Important guidelines:
60+
- Use ABSOLUTE PATHS when reading and writing files
61+
- Do NOT modify actual application logic files (.py, .js, .java source code), only
62+
modify IaC code, Dockerfiles, and dependency files (requirements.txt, pyproject.toml,
63+
package.json, pom.xml, build.gradle, *.csproj, etc.) as instructed by the guide.
64+
- Read application files if needed to understand the setup, but avoid modifying them
65+
66+
Args:
67+
service_platform: The AWS platform where the service runs.
68+
MUST be one of: 'ec2', 'ecs', 'lambda', 'eks' (lowercase, exact match).
69+
To help user determine: check their IaC for ECS services, Lambda functions, EKS deployments, or EC2 instances.
70+
service_language: The service's programming language.
71+
MUST be one of: 'python', 'nodejs', 'java', 'dotnet' (lowercase, exact match).
72+
IMPORTANT: Use 'nodejs' (not 'js', 'node', or 'javascript'), 'dotnet' (not 'csharp' or 'c#').
73+
To help user determine: check for package.json (nodejs), requirements.txt (python), pom.xml (java), or .csproj (dotnet).
74+
iac_directory: ABSOLUTE path to the Infrastructure as Code (IaC) directory (e.g., /home/user/project/infrastructure)
75+
app_directory: ABSOLUTE path to the application code directory (e.g., /home/user/project/app)
76+
77+
Returns:
78+
Markdown-formatted enablement guide with step-by-step instructions
79+
"""
80+
logger.debug(
81+
f'get_enablement_guide called: service_platform={service_platform}, service_language={service_language}, '
82+
f'iac_directory={iac_directory}, app_directory={app_directory}'
83+
)
84+
85+
# Normalize to lowercase
86+
platform_str = service_platform.lower().strip()
87+
language_str = service_language.lower().strip()
88+
89+
guides_dir = Path(__file__).parent / 'enablement_guides'
90+
template_file = (
91+
guides_dir / 'templates' / platform_str / f'{platform_str}-{language_str}-enablement.md'
92+
)
93+
94+
logger.debug(f'Looking for enablement guide: {template_file}')
95+
96+
# Validate that paths are absolute
97+
iac_path = Path(iac_directory)
98+
app_path = Path(app_directory)
99+
100+
if not iac_path.is_absolute() or not app_path.is_absolute():
101+
error_msg = (
102+
f'Error: iac_directory and app_directory must be absolute paths.\n\n'
103+
f'Received: {iac_directory} and {app_directory}\n'
104+
f'Please provide absolute paths (e.g., /home/user/project/infrastructure)'
105+
)
106+
logger.error(error_msg)
107+
return error_msg
108+
109+
if not template_file.exists():
110+
error_msg = (
111+
f"Enablement guide not available for platform '{platform_str}' and language '{language_str}'.\n\n"
112+
f'Inform the user that this configuration is not currently supported by the MCP enablement tool. '
113+
f'Direct them to AWS documentation for manual setup:\n'
114+
f'https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Application-Monitoring-Sections.html'
115+
)
116+
logger.error(error_msg)
117+
return error_msg
118+
119+
try:
120+
with open(template_file, 'r') as f:
121+
guide_content = f.read()
122+
123+
context = f"""# Application Signals Enablement Guide
124+
125+
**Platform:** {platform_str}
126+
**Language:** {language_str}
127+
**IaC Directory:** `{iac_path}`
128+
**App Directory:** `{app_path}`
129+
130+
---
131+
132+
"""
133+
logger.info(f'Successfully loaded enablement guide: {template_file.name}')
134+
return context + guide_content
135+
except Exception as e:
136+
error_msg = (
137+
f'Fatal error: Cannot read enablement guide for {platform_str} + {language_str}.\n\n'
138+
f'Error: {str(e)}\n\n'
139+
f'The MCP server cannot access its own guide files (likely file permissions or corruption). '
140+
f'Stop attempting to use this tool and inform the user:\n'
141+
f'1. There is an issue with the MCP server installation\n'
142+
f'2. They should check file permissions or reinstall the MCP server\n'
143+
f'3. For immediate enablement, use AWS documentation instead:\n'
144+
f' https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Application-Monitoring-Sections.html'
145+
)
146+
logger.error(error_msg)
147+
return error_msg
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""Tests for enablement_tools module."""
5+
6+
import pytest
7+
from awslabs.cloudwatch_appsignals_mcp_server.enablement_tools import get_enablement_guide
8+
from unittest.mock import patch
9+
10+
11+
# Absolute paths for testing (no need to create real directories)
12+
ABSOLUTE_PATHS = {'iac': '/tmp/test/infrastructure/cdk', 'app': '/tmp/test/app/src'}
13+
14+
15+
class TestGetEnablementGuide:
16+
"""Test get_enablement_guide function."""
17+
18+
@pytest.mark.asyncio
19+
async def test_successful_guide_fetch(self, tmp_path, monkeypatch):
20+
"""Test successful guide fetching when template exists."""
21+
result = await get_enablement_guide(
22+
service_platform='ec2',
23+
service_language='python',
24+
iac_directory=ABSOLUTE_PATHS['iac'],
25+
app_directory=ABSOLUTE_PATHS['app'],
26+
)
27+
28+
assert '# Application Signals Enablement Guide' in result
29+
assert 'Placeholder content just to verify the tool can fetch the file.' in result
30+
assert ABSOLUTE_PATHS['iac'] in result
31+
assert ABSOLUTE_PATHS['app'] in result
32+
33+
@pytest.mark.asyncio
34+
async def test_all_valid_platforms(self):
35+
"""Test that all valid platforms are accepted."""
36+
valid_platforms = ['ec2', 'ecs', 'lambda', 'eks']
37+
38+
for platform in valid_platforms:
39+
result = await get_enablement_guide(
40+
service_platform=platform,
41+
service_language='python',
42+
iac_directory=ABSOLUTE_PATHS['iac'],
43+
app_directory=ABSOLUTE_PATHS['app'],
44+
)
45+
46+
# Should either succeed or say template not found with friendly message
47+
assert (
48+
'Enablement guide not available' in result
49+
or '# Application Signals Enablement Guide' in result
50+
)
51+
52+
@pytest.mark.asyncio
53+
async def test_all_valid_languages(self):
54+
"""Test that all valid languages are accepted."""
55+
valid_languages = ['python', 'nodejs', 'java', 'dotnet']
56+
57+
for language in valid_languages:
58+
result = await get_enablement_guide(
59+
service_platform='ec2',
60+
service_language=language,
61+
iac_directory=ABSOLUTE_PATHS['iac'],
62+
app_directory=ABSOLUTE_PATHS['app'],
63+
)
64+
65+
# Should either succeed or say template not found with friendly message
66+
assert (
67+
'Enablement guide not available' in result
68+
or '# Application Signals Enablement Guide' in result
69+
)
70+
71+
@pytest.mark.asyncio
72+
async def test_relative_path_rejected(self):
73+
"""Test that relative paths are rejected with clear error message."""
74+
result = await get_enablement_guide(
75+
service_platform='ec2',
76+
service_language='python',
77+
iac_directory='infrastructure/cdk',
78+
app_directory=ABSOLUTE_PATHS['app'],
79+
)
80+
81+
assert 'Error: iac_directory and app_directory must be absolute paths' in result
82+
assert 'infrastructure/cdk' in result
83+
84+
@pytest.mark.asyncio
85+
async def test_relative_app_directory_rejected(self):
86+
"""Test that relative app directory is rejected with clear error message."""
87+
result = await get_enablement_guide(
88+
service_platform='ec2',
89+
service_language='python',
90+
iac_directory=ABSOLUTE_PATHS['iac'],
91+
app_directory='app/src',
92+
)
93+
94+
assert 'Error: iac_directory and app_directory must be absolute paths' in result
95+
assert 'app/src' in result
96+
97+
@pytest.mark.asyncio
98+
async def test_absolute_path_handling(self):
99+
"""Test that absolute paths are handled correctly."""
100+
result = await get_enablement_guide(
101+
service_platform='ec2',
102+
service_language='python',
103+
iac_directory=ABSOLUTE_PATHS['iac'],
104+
app_directory=ABSOLUTE_PATHS['app'],
105+
)
106+
107+
assert '# Application Signals Enablement Guide' in result
108+
assert ABSOLUTE_PATHS['iac'] in result
109+
assert ABSOLUTE_PATHS['app'] in result
110+
111+
@pytest.mark.asyncio
112+
async def test_unsupported_language_ruby(self):
113+
"""Test that unsupported language (ruby) returns friendly error message."""
114+
result = await get_enablement_guide(
115+
service_platform='ec2',
116+
service_language='ruby',
117+
iac_directory=ABSOLUTE_PATHS['iac'],
118+
app_directory=ABSOLUTE_PATHS['app'],
119+
)
120+
121+
assert 'Enablement guide not available' in result
122+
assert 'ruby' in result.lower()
123+
assert 'not currently supported' in result
124+
125+
@pytest.mark.asyncio
126+
async def test_unsupported_platform_k8s(self):
127+
"""Test that unsupported platform (k8s) returns friendly error message."""
128+
result = await get_enablement_guide(
129+
service_platform='k8s',
130+
service_language='python',
131+
iac_directory=ABSOLUTE_PATHS['iac'],
132+
app_directory=ABSOLUTE_PATHS['app'],
133+
)
134+
135+
assert 'Enablement guide not available' in result
136+
assert 'k8s' in result.lower()
137+
assert 'not currently supported' in result
138+
139+
@pytest.mark.asyncio
140+
async def test_case_insensitive_platform(self):
141+
"""Test that uppercase platform names are normalized to lowercase."""
142+
result = await get_enablement_guide(
143+
service_platform='EC2',
144+
service_language='python',
145+
iac_directory=ABSOLUTE_PATHS['iac'],
146+
app_directory=ABSOLUTE_PATHS['app'],
147+
)
148+
149+
# Should work the same as lowercase
150+
assert 'Error: iac_directory and app_directory must be absolute paths' not in result
151+
assert (
152+
'# Application Signals Enablement Guide' in result
153+
or 'Enablement guide not available' in result
154+
)
155+
156+
@pytest.mark.asyncio
157+
async def test_case_insensitive_language(self):
158+
"""Test that uppercase language names are normalized to lowercase."""
159+
result = await get_enablement_guide(
160+
service_platform='ec2',
161+
service_language='PYTHON',
162+
iac_directory=ABSOLUTE_PATHS['iac'],
163+
app_directory=ABSOLUTE_PATHS['app'],
164+
)
165+
166+
# Should work the same as lowercase
167+
assert 'Error: iac_directory and app_directory must be absolute paths' not in result
168+
assert (
169+
'# Application Signals Enablement Guide' in result
170+
or 'Enablement guide not available' in result
171+
)
172+
173+
@pytest.mark.asyncio
174+
async def test_whitespace_trimming(self):
175+
"""Test that leading/trailing whitespace is trimmed from inputs."""
176+
result = await get_enablement_guide(
177+
service_platform=' ec2 ',
178+
service_language=' python ',
179+
iac_directory=ABSOLUTE_PATHS['iac'],
180+
app_directory=ABSOLUTE_PATHS['app'],
181+
)
182+
183+
# Should work the same as trimmed input
184+
assert 'Error: iac_directory and app_directory must be absolute paths' not in result
185+
assert (
186+
'# Application Signals Enablement Guide' in result
187+
or 'Enablement guide not available' in result
188+
)
189+
190+
@pytest.mark.asyncio
191+
async def test_both_paths_relative(self):
192+
"""Test that error message shows both paths when both are relative."""
193+
result = await get_enablement_guide(
194+
service_platform='ec2',
195+
service_language='python',
196+
iac_directory='infrastructure/cdk',
197+
app_directory='app/src',
198+
)
199+
200+
assert 'Error: iac_directory and app_directory must be absolute paths' in result
201+
assert 'infrastructure/cdk' in result
202+
assert 'app/src' in result
203+
204+
@pytest.mark.asyncio
205+
async def test_file_read_error(self):
206+
"""Test that file read errors are handled gracefully with helpful message."""
207+
with patch('builtins.open', side_effect=PermissionError('Permission denied')):
208+
result = await get_enablement_guide(
209+
service_platform='ec2',
210+
service_language='python',
211+
iac_directory=ABSOLUTE_PATHS['iac'],
212+
app_directory=ABSOLUTE_PATHS['app'],
213+
)
214+
215+
assert 'Fatal error: Cannot read enablement guide' in result
216+
assert 'Permission denied' in result
217+
assert 'file permissions or reinstall' in result
218+
assert 'issue with the MCP server installation' in result

0 commit comments

Comments
 (0)