awslabs.finch-mcp-server 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,167 @@
1
+ """Common utility functions for the Finch MCP server.
2
+
3
+ This module provides shared utility functions used across the Finch MCP server,
4
+ including command execution and result formatting.
5
+
6
+ Note: These tools are intended for development and prototyping purposes only
7
+ and are not meant for production use cases.
8
+ """
9
+
10
+ import os
11
+ import re
12
+ import subprocess
13
+ import sys
14
+ from loguru import logger
15
+ from pathlib import Path
16
+ from typing import Any, Dict, List
17
+
18
+
19
+ def get_dangerous_patterns() -> List[str]:
20
+ """Get a list of dangerous patterns for command injection detection.
21
+
22
+ Returns:
23
+ List of dangerous patterns to check for
24
+
25
+ """
26
+ # Dangerous patterns that could indicate command injection attempts
27
+ # Separated by platform for better organization and maintainability
28
+ patterns = [
29
+ '|',
30
+ ';',
31
+ '&',
32
+ '&&',
33
+ '||', # Command chaining
34
+ '>',
35
+ '>>',
36
+ '<', # Redirection
37
+ '`',
38
+ '$(', # Command substitution
39
+ '--', # Double dash options
40
+ '/bin/',
41
+ '/usr/bin/', # Path references
42
+ '../',
43
+ './', # Directory traversal
44
+ # Unix/Linux specific dangerous patterns
45
+ 'sudo', # Privilege escalation
46
+ 'chmod',
47
+ 'chown', # File permission changes
48
+ 'su', # Switch user
49
+ 'bash',
50
+ 'sh',
51
+ 'zsh', # Shell execution
52
+ 'curl',
53
+ 'wget', # Network access
54
+ 'ssh',
55
+ 'scp', # Remote access
56
+ 'eval', # Command evaluation
57
+ 'source', # Script sourcing
58
+ # Windows specific dangerous patterns
59
+ 'cmd',
60
+ 'powershell',
61
+ 'pwsh', # Command shells
62
+ 'net', # Network commands
63
+ 'reg', # Registry access
64
+ 'runas', # Privilege escalation
65
+ 'del',
66
+ 'rmdir', # File deletion
67
+ 'taskkill', # Process termination
68
+ 'sc', # Service control
69
+ 'schtasks', # Scheduled tasks
70
+ 'wmic', # WMI commands
71
+ '%SYSTEMROOT%',
72
+ '%WINDIR%', # System directories
73
+ '.bat',
74
+ '.cmd',
75
+ '.ps1', # Script files
76
+ ]
77
+ return patterns
78
+
79
+
80
+ def execute_command(command: list, env=None) -> subprocess.CompletedProcess:
81
+ """Execute a command and return the result.
82
+
83
+ This is a utility function that handles the execution of CLI commands.
84
+ It sets up the proper environment variables (particularly HOME) and captures
85
+ both stdout and stderr output from the command.
86
+
87
+ Args:
88
+ command: List of command parts to execute (e.g., ['finch', 'vm', 'status'])
89
+ Note: Currently only 'finch' commands are allowed for security reasons.
90
+ env: Optional environment variables dictionary. If None, uses a copy of the
91
+ current environment with HOME set to the user's home directory.
92
+
93
+ Returns:
94
+ CompletedProcess object with command execution results, containing:
95
+ - returncode: The exit code of the command (0 typically means success)
96
+ - stdout: Standard output as text
97
+ - stderr: Standard error as text
98
+
99
+ Raises:
100
+ ValueError: If the command is not a finch command (doesn't start with 'finch')
101
+ or if dangerous patterns are detected in the command
102
+
103
+ """
104
+ if env is None:
105
+ env = os.environ.copy()
106
+ path = Path('~')
107
+ home_path = str(Path('~').expanduser())
108
+
109
+ if sys.platform == 'win32':
110
+ drive, path = os.path.splitdrive(home_path)
111
+ env['HOMEDRIVE'] = drive
112
+ env['HOMEPATH'] = path
113
+ else:
114
+ env['HOME'] = str(home_path)
115
+
116
+ # Security check: Only allow finch commands
117
+ if not command or command[0] != 'finch':
118
+ error_msg = f'Security violation: Only finch commands are allowed. Received: {command}'
119
+ logger.error(error_msg)
120
+ raise ValueError(error_msg)
121
+
122
+ dangerous_patterns = get_dangerous_patterns()
123
+ logger.debug(f'Checking for {len(dangerous_patterns)} dangerous patterns')
124
+
125
+ for pattern in dangerous_patterns:
126
+ for part in command:
127
+ escaped_pattern = re.escape(pattern)
128
+ regex_pattern = r'^' + escaped_pattern + r'$'
129
+
130
+ if re.search(regex_pattern, part):
131
+ error_msg = f'Security violation: Potentially dangerous pattern "{pattern}" detected in command: {part}'
132
+ logger.error(error_msg)
133
+ raise ValueError(error_msg)
134
+
135
+ result = subprocess.run(command, capture_output=True, text=True, env=env)
136
+ cmd_str = ' '.join(command)
137
+ logger.debug(f'Command executed: {cmd_str}')
138
+ logger.debug(f'Return code: {result.returncode}')
139
+ if result.stdout:
140
+ logger.debug(f'STDOUT: {result.stdout}')
141
+ if result.stderr:
142
+ logger.debug(f'STDERR: {result.stderr}')
143
+
144
+ return result
145
+
146
+
147
+ def format_result(status: str, message: str) -> Dict[str, Any]:
148
+ """Format a result dictionary with status and message.
149
+
150
+ This utility function creates a standardized response format used by
151
+ all the MCP tools. It ensures consistent response structure.
152
+
153
+ Args:
154
+ status: Status code string. Common values include:
155
+ - "success": Operation completed successfully
156
+ - "error": Operation failed
157
+ - "warn": Operation completed with warnings
158
+ - "info": Informational status
159
+ - "unknown": Status could not be determined
160
+ message: Descriptive message providing details about the result
161
+
162
+ Returns:
163
+ Dict[str, Any]: A dictionary with 'status', 'message'
164
+
165
+ """
166
+ result = {'status': status, 'message': message}
167
+ return result
@@ -0,0 +1,87 @@
1
+ """Utility functions for working with Amazon ECR repositories.
2
+
3
+ This module provides functions to check if an ECR repository exists and create it if needed.
4
+
5
+ Note: These tools are intended for development and prototyping purposes only and are not meant
6
+ for production use cases.
7
+ """
8
+
9
+ import boto3
10
+ from ..consts import STATUS_ERROR, STATUS_SUCCESS
11
+ from .common import format_result
12
+ from botocore.exceptions import ClientError
13
+ from loguru import logger
14
+ from typing import Dict, Optional
15
+
16
+
17
+ def create_ecr_repository(
18
+ repository_name: str,
19
+ region: Optional[str] = None,
20
+ ) -> Dict[str, str]:
21
+ """Check if an ECR repository exists and create it if it doesn't.
22
+
23
+ This function first checks if the specified ECR repository exists using boto3.
24
+ If the repository doesn't exist, it creates a new one with the given name.
25
+
26
+ Args:
27
+ repository_name: The name of the repository to check or create in ECR
28
+ region: AWS region for the ECR repository. If not provided, uses the default region
29
+ from AWS configuration
30
+
31
+ Returns:
32
+ Dict[str, Any]: A dictionary containing:
33
+ - status: "success" if the operation succeeded, "error" otherwise
34
+ - message: Details about the result of the operation
35
+
36
+ """
37
+ try:
38
+ ecr_client = boto3.client('ecr', region_name=region) if region else boto3.client('ecr')
39
+
40
+ try:
41
+ response = ecr_client.describe_repositories(repositoryNames=[repository_name])
42
+
43
+ if 'repositories' in response and len(response['repositories']) > 0:
44
+ repository = response['repositories'][0]
45
+ repository_uri = repository.get('repositoryUri', '')
46
+
47
+ logger.debug(
48
+ f"ECR repository '{repository_name}' already exists with URI: {repository_uri}"
49
+ )
50
+ return format_result(
51
+ STATUS_SUCCESS,
52
+ f"ECR repository '{repository_name}' already exists.",
53
+ )
54
+ except ClientError as e:
55
+ error_code = e.response.get('Error', {}).get('Code')
56
+
57
+ if error_code != 'RepositoryNotFoundException':
58
+ return format_result(
59
+ STATUS_ERROR,
60
+ f'Error checking ECR repository: {str(e)}',
61
+ )
62
+
63
+ response = ecr_client.create_repository(
64
+ repositoryName=repository_name,
65
+ imageScanningConfiguration={'scanOnPush': True},
66
+ imageTagMutability='IMMUTABLE',
67
+ )
68
+
69
+ repository = response.get('repository', {})
70
+ repository_uri = repository.get('repositoryUri', '')
71
+
72
+ logger.debug(f"Created ECR repository '{repository_name}' with URI: {repository_uri}")
73
+ return format_result(
74
+ STATUS_SUCCESS,
75
+ f"Successfully created ECR repository '{repository_name}' with URI: {repository_uri}.",
76
+ )
77
+
78
+ except ClientError as e:
79
+ return format_result(
80
+ STATUS_ERROR,
81
+ f"Failed to create ECR repository '{repository_name}': {str(e)}",
82
+ )
83
+ except Exception as e:
84
+ return format_result(
85
+ STATUS_ERROR,
86
+ f"Unexpected error creating ECR repository '{repository_name}': {str(e)}",
87
+ )
@@ -0,0 +1,125 @@
1
+ """Utility functions for pushing container images to repositories.
2
+
3
+ This module provides functions to push container images to repositories,
4
+ including Amazon ECR, and handle image tagging with hash values.
5
+
6
+ Note: These tools are intended for development and prototyping purposes only
7
+ and are not meant for production use cases.
8
+ """
9
+
10
+ import re
11
+ from ..consts import ECR_REFERENCE_PATTERN, REGION_PATTERN, STATUS_ERROR, STATUS_SUCCESS
12
+ from .common import execute_command, format_result
13
+ from loguru import logger
14
+ from typing import Dict
15
+
16
+
17
+ def is_ecr_repository(repository: str) -> bool:
18
+ """Validate if the provided repository URL is an ECR repository.
19
+
20
+ ECR repository URLs typically follow the pattern:
21
+ <aws_account_id>.dkr.ecr.<region>.amazonaws.com/<repository_name>:<tag>
22
+
23
+ Args:
24
+ repository: The repository URL to validate
25
+
26
+ Returns:
27
+ bool: True if the repository is an ECR repository, False otherwise
28
+
29
+ """
30
+ match = re.search(ECR_REFERENCE_PATTERN, repository)
31
+ if not match:
32
+ return False
33
+
34
+ # Validate that the region is a valid AWS region format (e.g., us-west-2, eu-central-1)
35
+ region = match.group(3)
36
+ return bool(re.match(REGION_PATTERN, region))
37
+
38
+
39
+ def get_image_short_hash(image: str) -> tuple[Dict[str, str], str]:
40
+ """Get the short hash (digest) of a container image.
41
+
42
+ Args:
43
+ image: The image name to get the hash for
44
+
45
+ Returns:
46
+ A tuple containing:
47
+ - Dict with status and message
48
+ - The short hash as a string (empty string if operation failed)
49
+
50
+ """
51
+ inspect_result = execute_command(['finch', 'image', 'inspect', image])
52
+
53
+ if inspect_result.returncode != 0:
54
+ # Log stderr for debugging
55
+ logger.debug(f'STDERR from image inspect: {inspect_result.stderr}')
56
+ error_result = format_result(
57
+ STATUS_ERROR,
58
+ f'Failed to get hash for image {image}: {inspect_result.stderr}',
59
+ )
60
+ return error_result, ''
61
+
62
+ hash_match = re.search(r'"Id":\s*"(sha256:[a-f0-9]+)"', inspect_result.stdout)
63
+
64
+ if not hash_match:
65
+ error_result = format_result(
66
+ STATUS_ERROR, f'Could not find hash in image inspect output for {image}'
67
+ )
68
+ return error_result, ''
69
+
70
+ image_hash = hash_match.group(1)
71
+ short_hash = image_hash[7:19] if image_hash.startswith('sha256:') else image_hash[:12]
72
+ logger.debug(f'Retrieved hash for image {image}: {image_hash}')
73
+ return format_result(
74
+ STATUS_SUCCESS,
75
+ f'Successfully retrieved hash for image {image}',
76
+ ), short_hash
77
+
78
+
79
+ def push_image(image: str) -> Dict[str, str]:
80
+ """Push an image to a repository, replacing the tag with the image hash.
81
+
82
+ Args:
83
+ image: The image to push
84
+
85
+ Returns:
86
+ Result of the push task
87
+
88
+ """
89
+ hash_result, short_hash = get_image_short_hash(image)
90
+
91
+ if hash_result['status'] != STATUS_SUCCESS:
92
+ return hash_result
93
+
94
+ tag_separator_index = image.rfind(':')
95
+ if tag_separator_index > 0:
96
+ repository = image[:tag_separator_index]
97
+ else:
98
+ repository = image
99
+
100
+ hash_tagged_image = f'{repository}:{short_hash}'
101
+
102
+ tag_result = execute_command(['finch', 'image', 'tag', image, hash_tagged_image])
103
+
104
+ if tag_result.returncode != 0:
105
+ # Log stderr for debugging
106
+ logger.debug(f'STDERR from image tag: {tag_result.stderr}')
107
+ return format_result(
108
+ STATUS_ERROR,
109
+ f'Failed to tag image with hash: {tag_result.stderr}',
110
+ )
111
+
112
+ push_result = execute_command(['finch', 'image', 'push', hash_tagged_image])
113
+
114
+ if push_result.returncode == 0:
115
+ logger.debug(f'STDOUT from image push: {push_result.stdout}')
116
+ return format_result(
117
+ STATUS_SUCCESS,
118
+ f'Successfully pushed image {hash_tagged_image} (original: {image}).',
119
+ )
120
+ else:
121
+ logger.debug(f'STDERR from image push: {push_result.stderr}')
122
+ return format_result(
123
+ STATUS_ERROR,
124
+ f'Failed to push image {hash_tagged_image}: {push_result.stderr}',
125
+ )