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.
- awslabs/__init__.py +12 -0
- awslabs/finch_mcp_server/__init__.py +14 -0
- awslabs/finch_mcp_server/consts.py +40 -0
- awslabs/finch_mcp_server/models.py +19 -0
- awslabs/finch_mcp_server/server.py +442 -0
- awslabs/finch_mcp_server/utils/__init__.py +16 -0
- awslabs/finch_mcp_server/utils/build.py +141 -0
- awslabs/finch_mcp_server/utils/common.py +167 -0
- awslabs/finch_mcp_server/utils/ecr.py +87 -0
- awslabs/finch_mcp_server/utils/push.py +125 -0
- awslabs/finch_mcp_server/utils/vm.py +417 -0
- awslabs_finch_mcp_server-0.1.1.dist-info/METADATA +212 -0
- awslabs_finch_mcp_server-0.1.1.dist-info/RECORD +17 -0
- awslabs_finch_mcp_server-0.1.1.dist-info/WHEEL +4 -0
- awslabs_finch_mcp_server-0.1.1.dist-info/entry_points.txt +2 -0
- awslabs_finch_mcp_server-0.1.1.dist-info/licenses/LICENSE +201 -0
- awslabs_finch_mcp_server-0.1.1.dist-info/licenses/NOTICE +2 -0
|
@@ -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
|
+
)
|