Skip to content

Commit 7bb88f5

Browse files
authored
feat: add source sanitization for Terraform examples (#18)
* docs: add rational on readme * docs: add MCP inspector and remove list of tools from readme * feat: remove relative paths from examples code
1 parent 9f8362d commit 7bb88f5

File tree

2 files changed

+90
-1
lines changed

2 files changed

+90
-1
lines changed

test/test_get_content.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ def mock_github_client(self):
3131
# Make synchronous methods actually synchronous
3232
client._extract_repo_from_module_id = Mock()
3333
client.match_file_patterns = Mock()
34+
# Ensure resolve_version returns a string, not a mock
35+
client.resolve_version = AsyncMock(return_value="1.0.0")
3436
return client
3537

3638
@pytest.fixture
@@ -1149,3 +1151,78 @@ def mock_match_patterns(
11491151
# Verify the matching logic worked correctly
11501152
# With the bug, this might fail if valid_include_matched logic is broken
11511153
assert "# main.tf content" in result
1154+
1155+
@pytest.mark.asyncio
1156+
async def test_get_content_source_replacement(
1157+
self, config, mock_github_client
1158+
):
1159+
"""Test that source = '../../' is replaced with module ID and version."""
1160+
# Setup
1161+
request = GetContentRequest(
1162+
module_id="terraform-ibm-modules/vpc/ibm",
1163+
path="examples/basic",
1164+
include_files=["main.tf"]
1165+
)
1166+
1167+
mock_github_client._extract_repo_from_module_id.return_value = (
1168+
"terraform-ibm-modules", "terraform-ibm-vpc"
1169+
)
1170+
mock_github_client.resolve_version.return_value = "v1.2.3"
1171+
1172+
# Mock directory contents
1173+
mock_github_client.get_directory_contents.return_value = [
1174+
{
1175+
"name": "main.tf",
1176+
"path": "examples/basic/main.tf",
1177+
"type": "file",
1178+
"download_url": "https://api.github.com/repos/terraform-ibm-modules/terraform-ibm-vpc/contents/examples/basic/main.tf"
1179+
}
1180+
]
1181+
1182+
# Mock file content with source = "../../" pattern
1183+
async def mock_get_file_content(owner, repo, path, ref):
1184+
return {
1185+
"name": "main.tf",
1186+
"path": "examples/basic/main.tf",
1187+
"content": "encoded_content",
1188+
"encoding": "base64",
1189+
"size": 200,
1190+
"decoded_content": '''module "vpc" {
1191+
source = "../../"
1192+
vpc_name = var.vpc_name
1193+
resource_group_id = var.resource_group_id
1194+
}
1195+
1196+
module "other" {
1197+
source = "../.."
1198+
other_param = "value"
1199+
}'''
1200+
}
1201+
1202+
mock_github_client.get_file_content.side_effect = mock_get_file_content
1203+
1204+
# Mock pattern matching
1205+
mock_github_client.match_file_patterns.return_value = [
1206+
{
1207+
"name": "main.tf",
1208+
"path": "examples/basic/main.tf",
1209+
"type": "file",
1210+
"download_url": "https://api.github.com/repos/terraform-ibm-modules/terraform-ibm-vpc/contents/examples/basic/main.tf"
1211+
}
1212+
]
1213+
1214+
# Execute
1215+
result = await get_content_impl(request, config, mock_github_client)
1216+
1217+
# Verify source replacement occurred
1218+
assert 'source = "terraform-ibm-modules/vpc/ibm"' in result
1219+
assert 'version = "1.2.3"' in result
1220+
1221+
# Verify original ../../ patterns are gone
1222+
assert 'source = "../../"' not in result
1223+
assert 'source = "../.."' not in result
1224+
1225+
# Verify basic structure is maintained
1226+
assert "## main.tf" in result
1227+
assert 'module "vpc"' in result
1228+
assert 'module "other"' in result

tim_mcp/tools/get_content.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,11 +116,23 @@ async def _get_content_with_client(
116116
for i, file_item in enumerate(filtered_files):
117117
result = results[i]
118118
if not isinstance(result, Exception):
119+
file_content = result.get("decoded_content", "")
120+
# If this is a Terraform file, replace source references
121+
# NOTE: This sanitizes examples by replacing relative paths with absolute module references
122+
if file_item["name"].endswith('.tf'):
123+
# Strip 'v' prefix from version for Terraform compatibility (GitHub uses v1.2.3, Terraform uses 1.2.3)
124+
terraform_version = resolved_version.lstrip('v')
125+
# Replace source = "../.." or source = "../../" with source = "{base_module_id}" and add version
126+
file_content = re.sub(
127+
r'source\s*=\s*"\.\.\/\.\.\/?"',
128+
f'source = "{base_module_id}"\n version = "{terraform_version}"',
129+
file_content
130+
)
119131
file_contents.append(
120132
{
121133
"name": file_item["name"],
122134
"path": file_item["path"],
123-
"content": result.get("decoded_content", ""),
135+
"content": file_content,
124136
"size": result.get("size", 0),
125137
}
126138
)

0 commit comments

Comments
 (0)