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
awslabs/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance
|
|
4
|
+
# with the License. A copy of the License is located at
|
|
5
|
+
#
|
|
6
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
#
|
|
8
|
+
# or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES
|
|
9
|
+
# OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions
|
|
10
|
+
# and limitations under the License.
|
|
11
|
+
|
|
12
|
+
"""AWSLabs package for MCP servers."""
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance
|
|
4
|
+
# with the License. A copy of the License is located at
|
|
5
|
+
#
|
|
6
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
#
|
|
8
|
+
# or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES
|
|
9
|
+
# OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions
|
|
10
|
+
# and limitations under the License.
|
|
11
|
+
|
|
12
|
+
"""awslabs.finch-mcp-server"""
|
|
13
|
+
|
|
14
|
+
__version__ = '0.1.0'
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Constants for the Finch MCP server.
|
|
2
|
+
|
|
3
|
+
This module defines constants used throughout the Finch MCP server.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Server name
|
|
11
|
+
SERVER_NAME = 'finch_mcp_server'
|
|
12
|
+
|
|
13
|
+
# Log file name
|
|
14
|
+
LOG_FILE = 'finch_server.log'
|
|
15
|
+
|
|
16
|
+
# VM states
|
|
17
|
+
VM_STATE_RUNNING = 'running'
|
|
18
|
+
VM_STATE_STOPPED = 'stopped'
|
|
19
|
+
VM_STATE_NONEXISTENT = 'nonexistent'
|
|
20
|
+
VM_STATE_UNKNOWN = 'unknown'
|
|
21
|
+
|
|
22
|
+
# Operation status
|
|
23
|
+
STATUS_SUCCESS = 'success'
|
|
24
|
+
STATUS_ERROR = 'error'
|
|
25
|
+
STATUS_WARNING = 'warning'
|
|
26
|
+
STATUS_INFO = 'info'
|
|
27
|
+
|
|
28
|
+
# AWS region pattern
|
|
29
|
+
REGION_PATTERN = r'^[a-zA-Z0-9][a-zA-Z0-9-_]*$'
|
|
30
|
+
|
|
31
|
+
# ECR repository pattern
|
|
32
|
+
ECR_REFERENCE_PATTERN = r'(\d{12})\.dkr[-.]ecr(\-fips)?\.([a-zA-Z0-9][a-zA-Z0-9-_]*)\.(on\.aws|amazonaws\.com(\.cn)?|sc2s\.sgov\.gov|c2s\.ic\.gov|cloud\.adc-e\.uk|csp\.hci\.ic\.gov)'
|
|
33
|
+
|
|
34
|
+
# Platform-specific configuration file paths
|
|
35
|
+
if sys.platform == 'win32':
|
|
36
|
+
# Windows path using %LocalAppData%
|
|
37
|
+
FINCH_YAML_PATH = os.path.join(os.environ.get('LOCALAPPDATA', ''), '.finch', 'finch.yaml')
|
|
38
|
+
else:
|
|
39
|
+
# macOS path
|
|
40
|
+
FINCH_YAML_PATH = '~/.finch/finch.yaml'
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Pydantic models for the Finch MCP server.
|
|
2
|
+
|
|
3
|
+
This module defines the data models used for request and response validation
|
|
4
|
+
in the Finch MCP server tools.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Result(BaseModel):
|
|
11
|
+
"""Base model for operation results.
|
|
12
|
+
|
|
13
|
+
This model only includes status and message fields, regardless of what additional
|
|
14
|
+
fields might be present in the input dictionary. This ensures that only these two
|
|
15
|
+
fields are returned to the user.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
status: str = Field(..., description="Status of the operation ('success', 'error', etc.)")
|
|
19
|
+
message: str = Field(..., description='Descriptive message about the result of the operation')
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
"""Finch MCP Server main module.
|
|
2
|
+
|
|
3
|
+
This module provides the MCP server implementation for Finch container operations.
|
|
4
|
+
|
|
5
|
+
Note: The tools provided by this MCP server are intended for development and prototyping
|
|
6
|
+
purposes only and are not meant for production use cases.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import sys
|
|
12
|
+
from awslabs.finch_mcp_server.consts import LOG_FILE, SERVER_NAME
|
|
13
|
+
|
|
14
|
+
# Import Pydantic models for input validation
|
|
15
|
+
from awslabs.finch_mcp_server.models import Result
|
|
16
|
+
from awslabs.finch_mcp_server.utils.build import build_image, contains_ecr_reference
|
|
17
|
+
from awslabs.finch_mcp_server.utils.common import format_result
|
|
18
|
+
from awslabs.finch_mcp_server.utils.ecr import create_ecr_repository
|
|
19
|
+
|
|
20
|
+
# Import utility functions from local modules
|
|
21
|
+
from awslabs.finch_mcp_server.utils.push import is_ecr_repository, push_image
|
|
22
|
+
from awslabs.finch_mcp_server.utils.vm import (
|
|
23
|
+
check_finch_installation,
|
|
24
|
+
configure_ecr,
|
|
25
|
+
get_vm_status,
|
|
26
|
+
initialize_vm,
|
|
27
|
+
is_vm_nonexistent,
|
|
28
|
+
is_vm_running,
|
|
29
|
+
is_vm_stopped,
|
|
30
|
+
start_stopped_vm,
|
|
31
|
+
stop_vm,
|
|
32
|
+
)
|
|
33
|
+
from loguru import logger
|
|
34
|
+
from mcp.server.fastmcp import FastMCP
|
|
35
|
+
from pydantic import Field
|
|
36
|
+
from typing import Any, Dict, List, Optional
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Configure loguru logger
|
|
40
|
+
def sensitive_data_filter(record):
|
|
41
|
+
"""Filter that redacts sensitive information from log messages.
|
|
42
|
+
|
|
43
|
+
This function processes log records to redact sensitive information such as
|
|
44
|
+
API keys, passwords, and credentials from the message.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
record: The log record to process
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
bool: True to allow the log record to be processed, False to filter it out
|
|
51
|
+
|
|
52
|
+
"""
|
|
53
|
+
# Define patterns for sensitive data detection
|
|
54
|
+
patterns = [
|
|
55
|
+
# AWS Access Key (20 character alphanumeric)
|
|
56
|
+
(re.compile(r'((?<![A-Z0-9])[A-Z0-9]{20}(?![A-Z0-9]))'), 'AWS_ACCESS_KEY_REDACTED'),
|
|
57
|
+
# AWS Secret Key (40 character base64)
|
|
58
|
+
(
|
|
59
|
+
re.compile(r'((?<![A-Za-z0-9/+=])[A-Za-z0-9/+=]{40}(?![A-Za-z0-9/+=]))'),
|
|
60
|
+
'AWS_SECRET_KEY_REDACTED',
|
|
61
|
+
),
|
|
62
|
+
# API Keys
|
|
63
|
+
(
|
|
64
|
+
re.compile(r'(api[_-]?key[=:]\s*[\'"]?)[^\'"\s]+([\'"]?)', re.IGNORECASE),
|
|
65
|
+
r'api_key=REDACTED',
|
|
66
|
+
),
|
|
67
|
+
# Passwords
|
|
68
|
+
(
|
|
69
|
+
re.compile(r'(password[=:]\s*[\'"]?)[^\'"\s]+([\'"]?)', re.IGNORECASE),
|
|
70
|
+
r'password=REDACTED',
|
|
71
|
+
),
|
|
72
|
+
# Secrets
|
|
73
|
+
(
|
|
74
|
+
re.compile(r'(secret[=:]\s*[\'"]?)[^\'"\s]+([\'"]?)', re.IGNORECASE),
|
|
75
|
+
r'secret=REDACTED',
|
|
76
|
+
),
|
|
77
|
+
# Tokens
|
|
78
|
+
(re.compile(r'(token[=:]\s*[\'"]?)[^\'"\s]+([\'"]?)', re.IGNORECASE), r'\1REDACTED\2'),
|
|
79
|
+
# URLs with credentials
|
|
80
|
+
(re.compile(r'(https?://)([^:@\s]+):([^:@\s]+)@'), r'\1REDACTED:REDACTED@'),
|
|
81
|
+
# JWT tokens (common format)
|
|
82
|
+
(
|
|
83
|
+
re.compile(r'eyJ[a-zA-Z0-9_-]{5,}\.eyJ[a-zA-Z0-9_-]{5,}\.[a-zA-Z0-9_-]{5,}'),
|
|
84
|
+
'JWT_TOKEN_REDACTED',
|
|
85
|
+
),
|
|
86
|
+
# OAuth tokens
|
|
87
|
+
(
|
|
88
|
+
re.compile(r'(oauth[_-]?token[=:]\s*[\'"]?)[^\'"\s]+([\'"]?)', re.IGNORECASE),
|
|
89
|
+
r'\1REDACTED\2',
|
|
90
|
+
),
|
|
91
|
+
# Generic credentials
|
|
92
|
+
(
|
|
93
|
+
re.compile(r'(credential[s]?[=:]\s*[\'"]?)[^\'"\s]+([\'"]?)', re.IGNORECASE),
|
|
94
|
+
r'\1REDACTED\2',
|
|
95
|
+
),
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
if 'message' in record:
|
|
100
|
+
message = record['message']
|
|
101
|
+
|
|
102
|
+
for pattern, replacement in patterns:
|
|
103
|
+
message = pattern.sub(replacement, message)
|
|
104
|
+
|
|
105
|
+
record['message'] = message
|
|
106
|
+
|
|
107
|
+
except Exception as e:
|
|
108
|
+
if 'message' in record:
|
|
109
|
+
record['message'] = (
|
|
110
|
+
f'{record["message"]} [SENSITIVE_DATA_FILTER_ERROR: Exception occurred during sensitive data filtering]'
|
|
111
|
+
)
|
|
112
|
+
else:
|
|
113
|
+
record['message'] = (
|
|
114
|
+
'[SENSITIVE_DATA_FILTER_ERROR: Exception occurred during sensitive data filtering]'
|
|
115
|
+
)
|
|
116
|
+
logger.debug(f'Error in sensitive_data_filter: {str(e)}')
|
|
117
|
+
|
|
118
|
+
# Return True to allow the log record to be processed
|
|
119
|
+
return True
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# Remove all default handlers then add our own
|
|
123
|
+
logger.remove()
|
|
124
|
+
|
|
125
|
+
log_level = os.environ.get('FASTMCP_LOG_LEVEL', 'INFO').upper()
|
|
126
|
+
logger.add(
|
|
127
|
+
LOG_FILE,
|
|
128
|
+
rotation='10 MB',
|
|
129
|
+
retention=7,
|
|
130
|
+
level=log_level,
|
|
131
|
+
format='{time:YYYY-MM-DD HH:mm:ss} | {level} | {name}:{function}:{line} | {message}',
|
|
132
|
+
filter=sensitive_data_filter,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Add a handler for stderr
|
|
136
|
+
logger.add(
|
|
137
|
+
sys.stderr,
|
|
138
|
+
level=log_level,
|
|
139
|
+
format='{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}',
|
|
140
|
+
filter=sensitive_data_filter,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
logger = logger.bind(name=SERVER_NAME)
|
|
144
|
+
|
|
145
|
+
# Initialize the MCP server
|
|
146
|
+
mcp = FastMCP(SERVER_NAME)
|
|
147
|
+
enable_aws_resource_write = False
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def ensure_vm_running() -> Dict[str, Any]:
|
|
151
|
+
"""Ensure that the Finch VM is running before performing operations.
|
|
152
|
+
|
|
153
|
+
This function checks the current status of the Finch VM and takes appropriate action:
|
|
154
|
+
- If the VM is nonexistent: Creates a new VM instance using 'finch vm init'
|
|
155
|
+
- If the VM is stopped: Starts the VM using 'finch vm start'
|
|
156
|
+
- If the VM is already running: Does nothing
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Dict[str, Any]: A dictionary containing:
|
|
160
|
+
- status (str): "success" if the VM is running or was started successfully,
|
|
161
|
+
"error" otherwise
|
|
162
|
+
- message (str): A descriptive message about the result of the operation
|
|
163
|
+
|
|
164
|
+
"""
|
|
165
|
+
try:
|
|
166
|
+
if sys.platform == 'linux':
|
|
167
|
+
logger.info('Linux OS detected. Finch does not use a VM on Linux...')
|
|
168
|
+
return format_result('success', 'Finch does not use a VM on Linux..')
|
|
169
|
+
|
|
170
|
+
status_result = get_vm_status()
|
|
171
|
+
|
|
172
|
+
if is_vm_nonexistent(status_result):
|
|
173
|
+
logger.info('Finch VM does not exist. Initializing...')
|
|
174
|
+
result = initialize_vm()
|
|
175
|
+
if result['status'] == 'error':
|
|
176
|
+
return result
|
|
177
|
+
return format_result('success', 'Finch VM was initialized successfully.')
|
|
178
|
+
elif is_vm_stopped(status_result):
|
|
179
|
+
logger.info('Finch VM is stopped. Starting it...')
|
|
180
|
+
result = start_stopped_vm()
|
|
181
|
+
if result['status'] == 'error':
|
|
182
|
+
return result
|
|
183
|
+
return format_result('success', 'Finch VM was started successfully.')
|
|
184
|
+
elif is_vm_running(status_result):
|
|
185
|
+
return format_result('success', 'Finch VM is already running.')
|
|
186
|
+
else:
|
|
187
|
+
return format_result(
|
|
188
|
+
'error',
|
|
189
|
+
f'Unknown VM status: status code {status_result.returncode}',
|
|
190
|
+
)
|
|
191
|
+
except Exception as e:
|
|
192
|
+
return format_result('error', f'Error ensuring Finch VM is running: {str(e)}')
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@mcp.tool()
|
|
196
|
+
async def finch_build_container_image(
|
|
197
|
+
dockerfile_path: str = Field(..., description='Absolute path to the Dockerfile'),
|
|
198
|
+
context_path: str = Field(..., description='Absolute path to the build context directory'),
|
|
199
|
+
tags: Optional[List[str]] = Field(
|
|
200
|
+
default=None,
|
|
201
|
+
description="List of tags to apply to the image (e.g., ['myimage:latest', 'myimage:v1'])",
|
|
202
|
+
),
|
|
203
|
+
platforms: Optional[List[str]] = Field(
|
|
204
|
+
default=None, description="List of target platforms (e.g., ['linux/amd64', 'linux/arm64'])"
|
|
205
|
+
),
|
|
206
|
+
target: Optional[str] = Field(default=None, description='Target build stage to build'),
|
|
207
|
+
no_cache: Optional[bool] = Field(default=False, description='Whether to disable cache'),
|
|
208
|
+
pull: Optional[bool] = Field(default=False, description='Whether to always pull base images'),
|
|
209
|
+
build_contexts: Optional[List[str]] = Field(
|
|
210
|
+
default=None, description='List of additional build contexts'
|
|
211
|
+
),
|
|
212
|
+
outputs: Optional[str] = Field(default=None, description='Output destination'),
|
|
213
|
+
cache_from: Optional[List[str]] = Field(
|
|
214
|
+
default=None, description='List of external cache sources'
|
|
215
|
+
),
|
|
216
|
+
quiet: Optional[bool] = Field(default=False, description='Whether to suppress build output'),
|
|
217
|
+
progress: Optional[str] = Field(default='auto', description='Type of progress output'),
|
|
218
|
+
) -> Result:
|
|
219
|
+
"""Build a container image using Finch.
|
|
220
|
+
|
|
221
|
+
This tool builds a Docker image using the specified Dockerfile and context directory.
|
|
222
|
+
It supports a range of build options including tags, platforms, and more.
|
|
223
|
+
If the Dockerfile contains references to ECR repositories, it verifies that
|
|
224
|
+
ecr login cred helper is properly configured before proceeding with the build.
|
|
225
|
+
|
|
226
|
+
Note: for ecr-login to work server needs access to AWS credentials/profile which are configured
|
|
227
|
+
in the server mcp configuration file.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
Result: An object containing:
|
|
231
|
+
- status (str): "success" if the operation succeeded, "error" otherwise
|
|
232
|
+
- message (str): A descriptive message about the result of the operation
|
|
233
|
+
|
|
234
|
+
Example response:
|
|
235
|
+
Result(status="success", message="Successfully built image from /path/to/Dockerfile")
|
|
236
|
+
|
|
237
|
+
"""
|
|
238
|
+
logger.info('tool-name: finch_build_container_image')
|
|
239
|
+
logger.info(f'tool-args: dockerfile_path={dockerfile_path}, context_path={context_path}')
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
finch_install_status = check_finch_installation()
|
|
243
|
+
if finch_install_status['status'] == 'error':
|
|
244
|
+
return Result(**finch_install_status)
|
|
245
|
+
|
|
246
|
+
if contains_ecr_reference(dockerfile_path):
|
|
247
|
+
logger.info('ECR reference detected in Dockerfile, configuring ECR login')
|
|
248
|
+
config_result, config_changed = configure_ecr()
|
|
249
|
+
if config_result['status'] == 'error':
|
|
250
|
+
return Result(**config_result)
|
|
251
|
+
if config_changed:
|
|
252
|
+
logger.info('ECR configuration changed, restarting VM')
|
|
253
|
+
stop_vm(force=True)
|
|
254
|
+
|
|
255
|
+
vm_status = ensure_vm_running()
|
|
256
|
+
if vm_status['status'] == 'error':
|
|
257
|
+
return Result(**vm_status)
|
|
258
|
+
|
|
259
|
+
result = build_image(
|
|
260
|
+
dockerfile_path=dockerfile_path,
|
|
261
|
+
context_path=context_path,
|
|
262
|
+
tags=tags,
|
|
263
|
+
platforms=platforms,
|
|
264
|
+
target=target,
|
|
265
|
+
no_cache=no_cache,
|
|
266
|
+
pull=pull,
|
|
267
|
+
build_contexts=build_contexts,
|
|
268
|
+
outputs=outputs,
|
|
269
|
+
cache_from=cache_from,
|
|
270
|
+
quiet=quiet,
|
|
271
|
+
progress=progress,
|
|
272
|
+
)
|
|
273
|
+
return Result(**result)
|
|
274
|
+
except Exception as e:
|
|
275
|
+
error_result = format_result('error', f'Error building Docker image: {str(e)}')
|
|
276
|
+
return Result(**error_result)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
@mcp.tool()
|
|
280
|
+
async def finch_push_image(
|
|
281
|
+
image: str = Field(
|
|
282
|
+
..., description='The full image name to push, including the repository URL and tag'
|
|
283
|
+
),
|
|
284
|
+
) -> Result:
|
|
285
|
+
"""Push a container image to a repository using finch, replacing the tag with the image hash.
|
|
286
|
+
|
|
287
|
+
If the image URL is an ECR repository, it verifies that ECR login cred helper is configured.
|
|
288
|
+
This tool gets the image hash, creates a new tag using the hash, and pushes the image with
|
|
289
|
+
the hash tag to the repository. If the image URL is an ECR repository, it verifies that
|
|
290
|
+
ECR login is properly configured before proceeding with the push.
|
|
291
|
+
|
|
292
|
+
The tool expects the image to be already built and available locally. It uses
|
|
293
|
+
'finch image inspect' to get the hash, 'finch image tag' to create a new tag,
|
|
294
|
+
and 'finch image push' to perform the actual push operation.
|
|
295
|
+
|
|
296
|
+
When the server is in read-only mode (which is the default unless --enable-aws-resource-write
|
|
297
|
+
is specified), this tool will return an error when pushing to ECR repositories.
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
Result: An object containing:
|
|
301
|
+
- status (str): "success" if the operation succeeded, "error" otherwise
|
|
302
|
+
- message (str): A descriptive message about the result of the operation
|
|
303
|
+
|
|
304
|
+
Example response:
|
|
305
|
+
Result(status="success", message="Successfully pushed image 123456789012.dkr.ecr.us-west-2.amazonaws.com/my-repo:abcdef123456 to ECR.")
|
|
306
|
+
|
|
307
|
+
"""
|
|
308
|
+
logger.info('tool-name: finch_push_image')
|
|
309
|
+
logger.info(f'tool-args: image={image}')
|
|
310
|
+
|
|
311
|
+
try:
|
|
312
|
+
finch_install_status = check_finch_installation()
|
|
313
|
+
if finch_install_status['status'] == 'error':
|
|
314
|
+
return Result(**finch_install_status)
|
|
315
|
+
|
|
316
|
+
is_ecr = is_ecr_repository(image)
|
|
317
|
+
if is_ecr:
|
|
318
|
+
# Check if AWS resource write is enabled for ECR pushes
|
|
319
|
+
if not enable_aws_resource_write:
|
|
320
|
+
logger.warning(
|
|
321
|
+
f'Attempt to push image to ECR "{image}" without AWS resource write enabled'
|
|
322
|
+
)
|
|
323
|
+
error_result = format_result(
|
|
324
|
+
'error', 'Server running in read-only mode, unable to push to ECR repository'
|
|
325
|
+
)
|
|
326
|
+
return Result(**error_result)
|
|
327
|
+
|
|
328
|
+
logger.info('ECR repository detected, configuring ECR login')
|
|
329
|
+
config_result, config_changed = configure_ecr()
|
|
330
|
+
if config_result['status'] == 'error':
|
|
331
|
+
return Result(**config_result)
|
|
332
|
+
if config_changed:
|
|
333
|
+
logger.info('ECR configuration changed, restarting VM')
|
|
334
|
+
stop_vm(force=True)
|
|
335
|
+
|
|
336
|
+
vm_status = ensure_vm_running()
|
|
337
|
+
if vm_status['status'] == 'error':
|
|
338
|
+
return Result(**vm_status)
|
|
339
|
+
|
|
340
|
+
result = push_image(image)
|
|
341
|
+
return Result(**result)
|
|
342
|
+
except Exception as e:
|
|
343
|
+
error_result = format_result('error', f'Error pushing image: {str(e)}')
|
|
344
|
+
return Result(**error_result)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def set_enable_aws_resource_write(enabled: bool):
|
|
348
|
+
"""Set whether AWS resource creation/modification is enabled.
|
|
349
|
+
|
|
350
|
+
When AWS resource write is disabled, certain operations like creating ECR repositories
|
|
351
|
+
will return an error.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
enabled (bool): True to enable AWS resource creation/modification, False to disable it
|
|
355
|
+
|
|
356
|
+
"""
|
|
357
|
+
global enable_aws_resource_write
|
|
358
|
+
enable_aws_resource_write = enabled
|
|
359
|
+
logger.info(f'AWS resource write enabled: {enable_aws_resource_write}')
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
@mcp.tool()
|
|
363
|
+
async def finch_create_ecr_repo(
|
|
364
|
+
repository_name: str = Field(
|
|
365
|
+
..., description='The name of the repository to check or create in ECR'
|
|
366
|
+
),
|
|
367
|
+
region: Optional[str] = Field(
|
|
368
|
+
default=None,
|
|
369
|
+
description='AWS region for the ECR repository. If not provided, uses the default region from AWS configuration',
|
|
370
|
+
),
|
|
371
|
+
) -> Result:
|
|
372
|
+
"""Check if an ECR repository exists and create it if it doesn't.
|
|
373
|
+
|
|
374
|
+
This tool checks if the specified ECR repository exists using boto3.
|
|
375
|
+
If the repository doesn't exist, it creates a new one with the given name.
|
|
376
|
+
The tool requires appropriate AWS credentials configured.
|
|
377
|
+
|
|
378
|
+
When the server is in read-only mode (which is the default unless --enable-aws-resource-write
|
|
379
|
+
is specified), this tool will return an error and will not create any repositories.
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
Result: An object containing:
|
|
383
|
+
- status (str): "success" if the operation succeeded, "error" otherwise
|
|
384
|
+
- message (str): A descriptive message about the result of the operation
|
|
385
|
+
|
|
386
|
+
Example response:
|
|
387
|
+
Result(status="success", message="Successfully created ECR repository 'my-app'.",
|
|
388
|
+
repository_uri="123456789012.dkr.ecr.us-west-2.amazonaws.com/my-app",
|
|
389
|
+
exists=False)
|
|
390
|
+
|
|
391
|
+
"""
|
|
392
|
+
logger.info('tool-name: finch_create_ecr_repo')
|
|
393
|
+
logger.info(f'tool-args: repository_name={repository_name}')
|
|
394
|
+
|
|
395
|
+
# Check if AWS resource write is enabled
|
|
396
|
+
if not enable_aws_resource_write:
|
|
397
|
+
logger.warning(
|
|
398
|
+
f'Attempt to create ECR repo "{repository_name}" without AWS resource write enabled'
|
|
399
|
+
)
|
|
400
|
+
error_result = format_result(
|
|
401
|
+
'error', 'Server running in read-only mode, unable to perform the action'
|
|
402
|
+
)
|
|
403
|
+
return Result(**error_result)
|
|
404
|
+
|
|
405
|
+
try:
|
|
406
|
+
result = create_ecr_repository(
|
|
407
|
+
repository_name=repository_name,
|
|
408
|
+
region=region,
|
|
409
|
+
)
|
|
410
|
+
return Result(**result)
|
|
411
|
+
except Exception as e:
|
|
412
|
+
error_result = format_result('error', f'Error checking/creating ECR repository: {str(e)}')
|
|
413
|
+
return Result(**error_result)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def main(enable_aws_resource_write: bool = False):
|
|
417
|
+
"""Run the Finch MCP server.
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
enable_aws_resource_write (bool, optional): Whether to enable AWS resource creation/modification. Defaults to False.
|
|
421
|
+
|
|
422
|
+
"""
|
|
423
|
+
# Set AWS resource write mode
|
|
424
|
+
set_enable_aws_resource_write(enable_aws_resource_write)
|
|
425
|
+
|
|
426
|
+
logger.info('Starting Finch MCP server')
|
|
427
|
+
logger.info(f'Logs will be written to: {LOG_FILE}')
|
|
428
|
+
mcp.run(transport='stdio')
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
if __name__ == '__main__':
|
|
432
|
+
import argparse
|
|
433
|
+
|
|
434
|
+
parser = argparse.ArgumentParser(description='Run the Finch MCP server')
|
|
435
|
+
parser.add_argument(
|
|
436
|
+
'--enable-aws-resource-write',
|
|
437
|
+
action='store_true',
|
|
438
|
+
help='Enable AWS resource creation and modification (disabled by default)',
|
|
439
|
+
)
|
|
440
|
+
args = parser.parse_args()
|
|
441
|
+
|
|
442
|
+
main(enable_aws_resource_write=args.enable_aws_resource_write)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance
|
|
4
|
+
# with the License. A copy of the License is located at
|
|
5
|
+
#
|
|
6
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
#
|
|
8
|
+
# or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES
|
|
9
|
+
# OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions
|
|
10
|
+
# and limitations under the License.
|
|
11
|
+
|
|
12
|
+
"""
|
|
13
|
+
Utility modules for the Finch MCP server.
|
|
14
|
+
|
|
15
|
+
This package contains utility modules for working with Finch container client.
|
|
16
|
+
"""
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Utility functions for building container images using Finch.
|
|
2
|
+
|
|
3
|
+
This module provides functions to build Docker images using Finch and check
|
|
4
|
+
if Dockerfiles contain references to ECR repositories.
|
|
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
|
+
from ..consts import ECR_REFERENCE_PATTERN, STATUS_ERROR, STATUS_SUCCESS
|
|
13
|
+
from .common import execute_command, format_result
|
|
14
|
+
from loguru import logger
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def contains_ecr_reference(dockerfile_path: str) -> bool:
|
|
19
|
+
"""Check if a Dockerfile contains references to ECR repositories.
|
|
20
|
+
|
|
21
|
+
This function scans the Dockerfile for `FROM` or other directives
|
|
22
|
+
that might reference an ECR repository.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
dockerfile_path (str): Path to the Dockerfile to check.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
bool: True if the Dockerfile contains ECR references, False otherwise.
|
|
29
|
+
|
|
30
|
+
"""
|
|
31
|
+
try:
|
|
32
|
+
if not os.path.exists(dockerfile_path):
|
|
33
|
+
logger.warning(f'Dockerfile not found at {dockerfile_path}')
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
with open(dockerfile_path, 'r') as f:
|
|
37
|
+
content = f.read()
|
|
38
|
+
return bool(re.search(ECR_REFERENCE_PATTERN, content))
|
|
39
|
+
except Exception as e:
|
|
40
|
+
logger.error(f'Error checking Dockerfile for ECR references: {str(e)}')
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def build_image(
|
|
45
|
+
dockerfile_path: str,
|
|
46
|
+
context_path: str,
|
|
47
|
+
tags: Optional[List[str]] = None,
|
|
48
|
+
platforms: Optional[List[str]] = None,
|
|
49
|
+
target: Optional[str] = None,
|
|
50
|
+
no_cache: Optional[bool] = False,
|
|
51
|
+
pull: Optional[bool] = False,
|
|
52
|
+
build_contexts: Optional[List[str]] = None,
|
|
53
|
+
outputs: Optional[str] = None,
|
|
54
|
+
cache_from: Optional[List[str]] = None,
|
|
55
|
+
quiet: Optional[bool] = False,
|
|
56
|
+
progress: Optional[str] = None,
|
|
57
|
+
) -> Dict[str, Any]:
|
|
58
|
+
"""Build a container image using Finch.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
dockerfile_path: Path to the Dockerfile
|
|
62
|
+
context_path: Path to the build context directory
|
|
63
|
+
tags: List of tags to apply to the image
|
|
64
|
+
platforms: List of target platforms
|
|
65
|
+
target: Target build stage
|
|
66
|
+
no_cache: Whether to disable cache
|
|
67
|
+
pull: Whether to always pull base images
|
|
68
|
+
build_contexts: List of additional build contexts
|
|
69
|
+
outputs: Output destination
|
|
70
|
+
cache_from: List of external cache sources
|
|
71
|
+
quiet: Whether to suppress build output
|
|
72
|
+
progress: Type of progress output
|
|
73
|
+
Returns:
|
|
74
|
+
Dict[str, Any]: Result of the build operation
|
|
75
|
+
|
|
76
|
+
"""
|
|
77
|
+
try:
|
|
78
|
+
# Check if Dockerfile exists
|
|
79
|
+
if not os.path.exists(dockerfile_path):
|
|
80
|
+
return format_result(STATUS_ERROR, f'Dockerfile not found at {dockerfile_path}')
|
|
81
|
+
|
|
82
|
+
if not os.path.exists(context_path):
|
|
83
|
+
return format_result(STATUS_ERROR, f'Context directory not found at {context_path}')
|
|
84
|
+
|
|
85
|
+
command = ['finch', 'image', 'build']
|
|
86
|
+
|
|
87
|
+
command.extend(['-f', dockerfile_path])
|
|
88
|
+
|
|
89
|
+
if tags:
|
|
90
|
+
for tag in tags:
|
|
91
|
+
command.extend(['-t', tag])
|
|
92
|
+
|
|
93
|
+
if platforms:
|
|
94
|
+
for platform in platforms:
|
|
95
|
+
command.extend(['--platform', platform])
|
|
96
|
+
|
|
97
|
+
if target:
|
|
98
|
+
command.extend(['--target', target])
|
|
99
|
+
|
|
100
|
+
if no_cache:
|
|
101
|
+
command.append('--no-cache')
|
|
102
|
+
|
|
103
|
+
if pull:
|
|
104
|
+
command.append('--pull')
|
|
105
|
+
|
|
106
|
+
if build_contexts:
|
|
107
|
+
for ctx in build_contexts:
|
|
108
|
+
command.extend(['--build-context', ctx])
|
|
109
|
+
|
|
110
|
+
if outputs:
|
|
111
|
+
command.extend(['--output', outputs])
|
|
112
|
+
|
|
113
|
+
if cache_from:
|
|
114
|
+
for cache in cache_from:
|
|
115
|
+
command.extend(['--cache-from', cache])
|
|
116
|
+
|
|
117
|
+
if quiet:
|
|
118
|
+
command.append('--quiet')
|
|
119
|
+
|
|
120
|
+
if progress:
|
|
121
|
+
command.extend(['--progress', progress])
|
|
122
|
+
|
|
123
|
+
command.append(context_path)
|
|
124
|
+
|
|
125
|
+
logger.info(f'Building image with command: {" ".join(command)}')
|
|
126
|
+
build_result = execute_command(command)
|
|
127
|
+
|
|
128
|
+
if build_result.returncode == 0:
|
|
129
|
+
# Log stdout for debugging
|
|
130
|
+
logger.debug(f'STDOUT from build: {build_result.stdout}')
|
|
131
|
+
return format_result(
|
|
132
|
+
STATUS_SUCCESS, f'Successfully built image from {dockerfile_path}'
|
|
133
|
+
)
|
|
134
|
+
else:
|
|
135
|
+
# Log stderr for debugging
|
|
136
|
+
logger.debug(f'STDERR from build: {build_result.stderr}')
|
|
137
|
+
return format_result(STATUS_ERROR, f'Failed to build image: {build_result.stderr}')
|
|
138
|
+
|
|
139
|
+
except Exception as e:
|
|
140
|
+
logger.error(f'Error building image: {str(e)}')
|
|
141
|
+
return format_result(STATUS_ERROR, f'Error building image: {str(e)}')
|