awslabs.s3-tables-mcp-server 0.0.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 +15 -0
- awslabs/s3_tables_mcp_server/__init__.py +18 -0
- awslabs/s3_tables_mcp_server/constants.py +167 -0
- awslabs/s3_tables_mcp_server/database.py +140 -0
- awslabs/s3_tables_mcp_server/engines/__init__.py +13 -0
- awslabs/s3_tables_mcp_server/engines/pyiceberg.py +239 -0
- awslabs/s3_tables_mcp_server/file_processor.py +485 -0
- awslabs/s3_tables_mcp_server/models.py +274 -0
- awslabs/s3_tables_mcp_server/namespaces.py +63 -0
- awslabs/s3_tables_mcp_server/resources.py +231 -0
- awslabs/s3_tables_mcp_server/s3_operations.py +43 -0
- awslabs/s3_tables_mcp_server/server.py +821 -0
- awslabs/s3_tables_mcp_server/table_buckets.py +136 -0
- awslabs/s3_tables_mcp_server/tables.py +307 -0
- awslabs/s3_tables_mcp_server/utils.py +139 -0
- awslabs_s3_tables_mcp_server-0.0.1.dist-info/METADATA +216 -0
- awslabs_s3_tables_mcp_server-0.0.1.dist-info/RECORD +21 -0
- awslabs_s3_tables_mcp_server-0.0.1.dist-info/WHEEL +4 -0
- awslabs_s3_tables_mcp_server-0.0.1.dist-info/entry_points.txt +2 -0
- awslabs_s3_tables_mcp_server-0.0.1.dist-info/licenses/LICENSE +175 -0
- awslabs_s3_tables_mcp_server-0.0.1.dist-info/licenses/NOTICE +2 -0
@@ -0,0 +1,821 @@
|
|
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
|
+
"""AWS S3 Tables MCP Server implementation.
|
16
|
+
|
17
|
+
This server provides a Model Context Protocol (MCP) interface for managing AWS S3 Tables,
|
18
|
+
enabling programmatic access to create, manage, and interact with S3-based table storage.
|
19
|
+
It supports operations for table buckets, namespaces, and individual S3 tables.
|
20
|
+
"""
|
21
|
+
|
22
|
+
import argparse
|
23
|
+
import functools
|
24
|
+
import json
|
25
|
+
import os
|
26
|
+
import platform
|
27
|
+
import sys
|
28
|
+
import traceback
|
29
|
+
from .utils import set_user_agent_mode
|
30
|
+
|
31
|
+
# Import modular components
|
32
|
+
from awslabs.s3_tables_mcp_server import (
|
33
|
+
__version__,
|
34
|
+
database,
|
35
|
+
file_processor,
|
36
|
+
namespaces,
|
37
|
+
resources,
|
38
|
+
s3_operations,
|
39
|
+
table_buckets,
|
40
|
+
tables,
|
41
|
+
)
|
42
|
+
from awslabs.s3_tables_mcp_server.constants import (
|
43
|
+
NAMESPACE_NAME_FIELD,
|
44
|
+
QUERY_FIELD,
|
45
|
+
REGION_NAME_FIELD,
|
46
|
+
S3_URL_FIELD,
|
47
|
+
TABLE_BUCKET_ARN_FIELD,
|
48
|
+
TABLE_BUCKET_NAME_PATTERN,
|
49
|
+
TABLE_NAME_FIELD,
|
50
|
+
)
|
51
|
+
from datetime import datetime, timezone
|
52
|
+
from mcp.server.fastmcp import FastMCP
|
53
|
+
from pydantic import Field
|
54
|
+
from typing import Annotated, Any, Callable, Dict, Optional
|
55
|
+
|
56
|
+
|
57
|
+
class S3TablesMCPServer(FastMCP):
|
58
|
+
"""Extended FastMCP server with write operation control."""
|
59
|
+
|
60
|
+
def __init__(self, *args, **kwargs):
|
61
|
+
"""Initialize the S3 Tables MCP server with write operation control.
|
62
|
+
|
63
|
+
Args:
|
64
|
+
*args: Positional arguments passed to FastMCP
|
65
|
+
**kwargs: Keyword arguments passed to FastMCP
|
66
|
+
"""
|
67
|
+
super().__init__(*args, **kwargs)
|
68
|
+
self.allow_write: bool = False
|
69
|
+
|
70
|
+
os_name = platform.system().lower()
|
71
|
+
if os_name == 'darwin':
|
72
|
+
self.log_dir = os.path.expanduser('~/Library/Logs')
|
73
|
+
elif os_name == 'windows':
|
74
|
+
self.log_dir = os.path.expanduser('~/AppData/Local/Logs')
|
75
|
+
else:
|
76
|
+
self.log_dir = os.path.expanduser('~/.local/share/s3-tables-mcp-server/logs/')
|
77
|
+
|
78
|
+
|
79
|
+
# Initialize FastMCP app
|
80
|
+
app = S3TablesMCPServer(
|
81
|
+
name='s3-tables-server',
|
82
|
+
instructions='A Model Context Protocol (MCP) server that enables programmatic access to AWS S3 Tables. This server provides a comprehensive interface for creating, managing, and interacting with S3-based table storage, supporting operations for table buckets, namespaces, and individual S3 tables. It integrates with Amazon Athena for SQL query execution, allowing both read and write operations on your S3 Tables data.',
|
83
|
+
version=__version__,
|
84
|
+
)
|
85
|
+
|
86
|
+
|
87
|
+
def write_operation(func: Callable) -> Callable:
|
88
|
+
"""Decorator to check if write operations are allowed.
|
89
|
+
|
90
|
+
Args:
|
91
|
+
func: The function to decorate
|
92
|
+
|
93
|
+
Returns:
|
94
|
+
The decorated function
|
95
|
+
|
96
|
+
Raises:
|
97
|
+
ValueError: If write operations are not allowed
|
98
|
+
"""
|
99
|
+
|
100
|
+
@functools.wraps(func)
|
101
|
+
async def wrapper(*args, **kwargs):
|
102
|
+
if not app.allow_write:
|
103
|
+
raise ValueError('Operation not permitted: Server is configured in read-only mode')
|
104
|
+
return await func(*args, **kwargs)
|
105
|
+
|
106
|
+
return wrapper
|
107
|
+
|
108
|
+
|
109
|
+
def log_tool_call_with_response(func):
|
110
|
+
"""Decorator to log tool call, response, and errors, using the function name automatically. Skips logging during tests if MCP_SERVER_DISABLE_LOGGING is set."""
|
111
|
+
|
112
|
+
@functools.wraps(func)
|
113
|
+
async def wrapper(*args, **kwargs):
|
114
|
+
# Disable logging during tests
|
115
|
+
if os.environ.get('PYTEST_CURRENT_TEST') or os.environ.get('MCP_SERVER_DISABLE_LOGGING'):
|
116
|
+
return await func(*args, **kwargs)
|
117
|
+
tool_name = func.__name__
|
118
|
+
# Log the call
|
119
|
+
try:
|
120
|
+
os.makedirs(app.log_dir, exist_ok=True)
|
121
|
+
log_file = os.path.join(app.log_dir, 'mcp-server-awslabs.s3-tables-mcp-server.log')
|
122
|
+
log_entry = {
|
123
|
+
'timestamp': datetime.now(timezone.utc).isoformat(),
|
124
|
+
'tool': tool_name,
|
125
|
+
'event': 'call',
|
126
|
+
'args': args,
|
127
|
+
'kwargs': kwargs,
|
128
|
+
'mcp_version': __version__,
|
129
|
+
}
|
130
|
+
with open(log_file, 'a') as f:
|
131
|
+
f.write(json.dumps(log_entry, default=str) + '\n')
|
132
|
+
except Exception as e:
|
133
|
+
print(
|
134
|
+
f"ERROR: Failed to create or write to log file in directory '{app.log_dir}': {e}",
|
135
|
+
file=sys.stderr,
|
136
|
+
)
|
137
|
+
sys.exit(1)
|
138
|
+
# Execute the function and log response or error
|
139
|
+
try:
|
140
|
+
response = await func(*args, **kwargs)
|
141
|
+
try:
|
142
|
+
log_entry = {
|
143
|
+
'timestamp': datetime.now(timezone.utc).isoformat(),
|
144
|
+
'tool': tool_name,
|
145
|
+
'event': 'response',
|
146
|
+
'response': response,
|
147
|
+
'mcp_version': __version__,
|
148
|
+
}
|
149
|
+
with open(log_file, 'a') as f:
|
150
|
+
f.write(json.dumps(log_entry, default=str) + '\n')
|
151
|
+
except Exception as e:
|
152
|
+
print(
|
153
|
+
f"ERROR: Failed to log response in directory '{app.log_dir}': {e}",
|
154
|
+
file=sys.stderr,
|
155
|
+
)
|
156
|
+
return response
|
157
|
+
except Exception as e:
|
158
|
+
tb = traceback.format_exc()
|
159
|
+
try:
|
160
|
+
log_entry = {
|
161
|
+
'timestamp': datetime.now(timezone.utc).isoformat(),
|
162
|
+
'tool': tool_name,
|
163
|
+
'event': 'error',
|
164
|
+
'error': str(e),
|
165
|
+
'traceback': tb,
|
166
|
+
'mcp_version': __version__,
|
167
|
+
}
|
168
|
+
with open(log_file, 'a') as f:
|
169
|
+
f.write(json.dumps(log_entry) + '\n')
|
170
|
+
except Exception as log_e:
|
171
|
+
print(
|
172
|
+
f"ERROR: Failed to log error in directory '{app.log_dir}': {log_e}",
|
173
|
+
file=sys.stderr,
|
174
|
+
)
|
175
|
+
raise
|
176
|
+
|
177
|
+
return wrapper
|
178
|
+
|
179
|
+
|
180
|
+
def log_tool_call(tool_name, *args, **kwargs):
|
181
|
+
"""Log a tool call with its arguments and metadata to the server log file.
|
182
|
+
|
183
|
+
Args:
|
184
|
+
tool_name (str): The name of the tool being called.
|
185
|
+
*args: Positional arguments passed to the tool.
|
186
|
+
**kwargs: Keyword arguments passed to the tool.
|
187
|
+
"""
|
188
|
+
try:
|
189
|
+
os.makedirs(app.log_dir, exist_ok=True)
|
190
|
+
log_file = os.path.join(app.log_dir, 'mcp-server-awslabs.s3-tables-mcp-server.log')
|
191
|
+
log_entry = {
|
192
|
+
'timestamp': datetime.now(timezone.utc).isoformat(),
|
193
|
+
'tool': tool_name,
|
194
|
+
'args': args,
|
195
|
+
'kwargs': kwargs,
|
196
|
+
'mcp_version': __version__,
|
197
|
+
}
|
198
|
+
with open(log_file, 'a') as f:
|
199
|
+
f.write(json.dumps(log_entry) + '\n')
|
200
|
+
except Exception as e:
|
201
|
+
print(
|
202
|
+
f"ERROR: Failed to create or write to log file in directory '{app.log_dir}': {e}",
|
203
|
+
file=sys.stderr,
|
204
|
+
)
|
205
|
+
sys.exit(1)
|
206
|
+
|
207
|
+
|
208
|
+
@app.tool()
|
209
|
+
@log_tool_call_with_response
|
210
|
+
async def list_table_buckets(
|
211
|
+
region_name: Annotated[Optional[str], REGION_NAME_FIELD] = None,
|
212
|
+
) -> str:
|
213
|
+
"""List all S3 table buckets for your AWS account.
|
214
|
+
|
215
|
+
Permissions:
|
216
|
+
You must have the s3tables:ListTableBuckets permission to use this operation.
|
217
|
+
"""
|
218
|
+
return await resources.list_table_buckets_resource(region_name=region_name)
|
219
|
+
|
220
|
+
|
221
|
+
@app.tool()
|
222
|
+
@log_tool_call_with_response
|
223
|
+
async def list_namespaces(region_name: Annotated[Optional[str], REGION_NAME_FIELD] = None) -> str:
|
224
|
+
"""List all namespaces across all S3 table buckets.
|
225
|
+
|
226
|
+
Permissions:
|
227
|
+
You must have the s3tables:ListNamespaces permission to use this operation.
|
228
|
+
"""
|
229
|
+
return await resources.list_namespaces_resource(region_name=region_name)
|
230
|
+
|
231
|
+
|
232
|
+
@app.tool()
|
233
|
+
@log_tool_call_with_response
|
234
|
+
async def list_tables(region_name: Annotated[Optional[str], REGION_NAME_FIELD] = None) -> str:
|
235
|
+
"""List all S3 tables across all table buckets and namespaces.
|
236
|
+
|
237
|
+
Permissions:
|
238
|
+
You must have the s3tables:ListTables permission to use this operation.
|
239
|
+
"""
|
240
|
+
return await resources.list_tables_resource(region_name=region_name)
|
241
|
+
|
242
|
+
|
243
|
+
@app.tool()
|
244
|
+
@log_tool_call_with_response
|
245
|
+
@write_operation
|
246
|
+
async def create_table_bucket(
|
247
|
+
name: Annotated[
|
248
|
+
str,
|
249
|
+
Field(
|
250
|
+
...,
|
251
|
+
description='Name of the table bucket to create. Must be 3-63 characters long and contain only lowercase letters, numbers, and hyphens.',
|
252
|
+
min_length=3,
|
253
|
+
max_length=63,
|
254
|
+
pattern=TABLE_BUCKET_NAME_PATTERN,
|
255
|
+
),
|
256
|
+
],
|
257
|
+
region_name: Annotated[Optional[str], REGION_NAME_FIELD] = None,
|
258
|
+
):
|
259
|
+
"""Creates an S3 table bucket.
|
260
|
+
|
261
|
+
Permissions:
|
262
|
+
You must have the s3tables:CreateTableBucket permission to use this operation.
|
263
|
+
"""
|
264
|
+
return await table_buckets.create_table_bucket(name=name, region_name=region_name)
|
265
|
+
|
266
|
+
|
267
|
+
@app.tool()
|
268
|
+
@log_tool_call_with_response
|
269
|
+
@write_operation
|
270
|
+
async def create_namespace(
|
271
|
+
table_bucket_arn: Annotated[str, TABLE_BUCKET_ARN_FIELD],
|
272
|
+
namespace: Annotated[str, NAMESPACE_NAME_FIELD],
|
273
|
+
region_name: Annotated[Optional[str], REGION_NAME_FIELD] = None,
|
274
|
+
):
|
275
|
+
"""Create a new namespace in an S3 table bucket.
|
276
|
+
|
277
|
+
Creates a namespace. A namespace is a logical grouping of tables within your S3 table bucket,
|
278
|
+
which you can use to organize S3 tables.
|
279
|
+
|
280
|
+
Permissions:
|
281
|
+
You must have the s3tables:CreateNamespace permission to use this operation.
|
282
|
+
"""
|
283
|
+
return await namespaces.create_namespace(
|
284
|
+
table_bucket_arn=table_bucket_arn, namespace=namespace, region_name=region_name
|
285
|
+
)
|
286
|
+
|
287
|
+
|
288
|
+
@app.tool()
|
289
|
+
@log_tool_call_with_response
|
290
|
+
@write_operation
|
291
|
+
async def create_table(
|
292
|
+
table_bucket_arn: Annotated[str, TABLE_BUCKET_ARN_FIELD],
|
293
|
+
namespace: Annotated[str, NAMESPACE_NAME_FIELD],
|
294
|
+
name: Annotated[str, TABLE_NAME_FIELD],
|
295
|
+
format: Annotated[
|
296
|
+
str, Field('ICEBERG', description='The format for the S3 table.', pattern=r'ICEBERG')
|
297
|
+
] = 'ICEBERG',
|
298
|
+
metadata: Annotated[
|
299
|
+
Optional[Dict[str, Any]], Field(None, description='The metadata for the S3 table.')
|
300
|
+
] = None,
|
301
|
+
region_name: Annotated[Optional[str], REGION_NAME_FIELD] = None,
|
302
|
+
):
|
303
|
+
"""Create a new S3 table in an S3 table bucket.
|
304
|
+
|
305
|
+
Creates a new S3 table associated with the given S3 namespace in an S3 table bucket.
|
306
|
+
The S3 table can be configured with specific format and metadata settings. Metadata contains the schema of the table. Use double type for decimals.
|
307
|
+
Do not use the metadata parameter if the schema is unclear.
|
308
|
+
|
309
|
+
Example of S3 table metadata:
|
310
|
+
{
|
311
|
+
"metadata": {
|
312
|
+
"iceberg": {
|
313
|
+
"schema": {
|
314
|
+
"type": "struct",
|
315
|
+
"fields": [{
|
316
|
+
"id": 1,
|
317
|
+
"name": "customer_id",
|
318
|
+
"type": "long",
|
319
|
+
"required": true
|
320
|
+
},
|
321
|
+
{
|
322
|
+
"id": 2,
|
323
|
+
"name": "customer_name",
|
324
|
+
"type": "string",
|
325
|
+
"required": true
|
326
|
+
},
|
327
|
+
{
|
328
|
+
"id": 3,
|
329
|
+
"name": "customer_balance",
|
330
|
+
"type": "double",
|
331
|
+
"required": false
|
332
|
+
}
|
333
|
+
]
|
334
|
+
},
|
335
|
+
"partition-spec": [
|
336
|
+
{
|
337
|
+
"source-id": 1,
|
338
|
+
"field-id": 1000,
|
339
|
+
"transform": "month",
|
340
|
+
"name": "sale_date_month"
|
341
|
+
}
|
342
|
+
],
|
343
|
+
"table-properties": {
|
344
|
+
"description": "Customer information table with customer_id for joining with transactions"
|
345
|
+
}
|
346
|
+
}
|
347
|
+
}
|
348
|
+
}
|
349
|
+
|
350
|
+
Permissions:
|
351
|
+
You must have the s3tables:CreateTable permission to use this operation.
|
352
|
+
If using metadata parameter, you must have the s3tables:PutTableData permission.
|
353
|
+
"""
|
354
|
+
from awslabs.s3_tables_mcp_server.models import OpenTableFormat, TableMetadata
|
355
|
+
|
356
|
+
# Convert string parameter to enum value
|
357
|
+
format_enum = OpenTableFormat(format) if format != 'ICEBERG' else OpenTableFormat.ICEBERG
|
358
|
+
|
359
|
+
# Convert metadata dict to TableMetadata if provided
|
360
|
+
table_metadata = TableMetadata.model_validate(metadata) if metadata else None
|
361
|
+
|
362
|
+
return await tables.create_table(
|
363
|
+
table_bucket_arn=table_bucket_arn,
|
364
|
+
namespace=namespace,
|
365
|
+
name=name,
|
366
|
+
format=format_enum,
|
367
|
+
metadata=table_metadata,
|
368
|
+
region_name=region_name,
|
369
|
+
)
|
370
|
+
|
371
|
+
|
372
|
+
@app.tool()
|
373
|
+
@log_tool_call_with_response
|
374
|
+
async def get_table_maintenance_config(
|
375
|
+
table_bucket_arn: Annotated[str, TABLE_BUCKET_ARN_FIELD],
|
376
|
+
namespace: Annotated[str, NAMESPACE_NAME_FIELD],
|
377
|
+
name: Annotated[str, TABLE_NAME_FIELD],
|
378
|
+
region_name: Annotated[Optional[str], REGION_NAME_FIELD] = None,
|
379
|
+
):
|
380
|
+
"""Get details about the maintenance configuration of a table.
|
381
|
+
|
382
|
+
Gets details about the maintenance configuration of a table. For more information, see S3 Tables maintenance in the Amazon Simple Storage Service User Guide.
|
383
|
+
|
384
|
+
Permissions:
|
385
|
+
You must have the s3tables:GetTableMaintenanceConfiguration permission to use this operation.
|
386
|
+
"""
|
387
|
+
return await tables.get_table_maintenance_configuration(
|
388
|
+
table_bucket_arn=table_bucket_arn, namespace=namespace, name=name, region_name=region_name
|
389
|
+
)
|
390
|
+
|
391
|
+
|
392
|
+
@app.tool()
|
393
|
+
@log_tool_call_with_response
|
394
|
+
async def get_maintenance_job_status(
|
395
|
+
table_bucket_arn: Annotated[str, TABLE_BUCKET_ARN_FIELD],
|
396
|
+
namespace: Annotated[str, NAMESPACE_NAME_FIELD],
|
397
|
+
name: Annotated[str, TABLE_NAME_FIELD],
|
398
|
+
region_name: Annotated[Optional[str], REGION_NAME_FIELD] = None,
|
399
|
+
):
|
400
|
+
"""Get the status of a maintenance job for a table.
|
401
|
+
|
402
|
+
Gets the status of a maintenance job for a table. For more information, see S3 Tables maintenance in the Amazon Simple Storage Service User Guide.
|
403
|
+
|
404
|
+
Permissions:
|
405
|
+
You must have the s3tables:GetTableMaintenanceJobStatus permission to use this operation.
|
406
|
+
"""
|
407
|
+
return await tables.get_table_maintenance_job_status(
|
408
|
+
table_bucket_arn=table_bucket_arn, namespace=namespace, name=name, region_name=region_name
|
409
|
+
)
|
410
|
+
|
411
|
+
|
412
|
+
@app.tool()
|
413
|
+
@log_tool_call_with_response
|
414
|
+
async def get_table_metadata_location(
|
415
|
+
table_bucket_arn: Annotated[str, TABLE_BUCKET_ARN_FIELD],
|
416
|
+
namespace: Annotated[str, NAMESPACE_NAME_FIELD],
|
417
|
+
name: Annotated[str, TABLE_NAME_FIELD],
|
418
|
+
region_name: Annotated[Optional[str], REGION_NAME_FIELD] = None,
|
419
|
+
):
|
420
|
+
"""Get the location of the S3 table metadata.
|
421
|
+
|
422
|
+
Gets the S3 URI location of the table metadata, which contains the schema and other
|
423
|
+
table configuration information.
|
424
|
+
|
425
|
+
Permissions:
|
426
|
+
You must have the s3tables:GetTableMetadataLocation permission to use this operation.
|
427
|
+
"""
|
428
|
+
return await tables.get_table_metadata_location(
|
429
|
+
table_bucket_arn=table_bucket_arn, namespace=namespace, name=name, region_name=region_name
|
430
|
+
)
|
431
|
+
|
432
|
+
|
433
|
+
@app.tool()
|
434
|
+
@log_tool_call_with_response
|
435
|
+
@write_operation
|
436
|
+
async def rename_table(
|
437
|
+
table_bucket_arn: Annotated[str, TABLE_BUCKET_ARN_FIELD],
|
438
|
+
namespace: Annotated[str, NAMESPACE_NAME_FIELD],
|
439
|
+
name: Annotated[str, TABLE_NAME_FIELD],
|
440
|
+
new_name: Annotated[Optional[str], TABLE_NAME_FIELD] = None,
|
441
|
+
new_namespace_name: Annotated[Optional[str], NAMESPACE_NAME_FIELD] = None,
|
442
|
+
version_token: Annotated[
|
443
|
+
Optional[str],
|
444
|
+
Field(
|
445
|
+
None,
|
446
|
+
description='The version token of the S3 table. Must be 1-2048 characters long.',
|
447
|
+
min_length=1,
|
448
|
+
max_length=2048,
|
449
|
+
),
|
450
|
+
] = None,
|
451
|
+
region_name: Annotated[Optional[str], REGION_NAME_FIELD] = None,
|
452
|
+
):
|
453
|
+
"""Rename an S3 table or move it to a different S3 namespace.
|
454
|
+
|
455
|
+
Renames an S3 table or moves it to a different S3 namespace within the same S3 table bucket.
|
456
|
+
This operation maintains the table's data and configuration while updating its location.
|
457
|
+
|
458
|
+
Permissions:
|
459
|
+
You must have the s3tables:RenameTable permission to use this operation.
|
460
|
+
"""
|
461
|
+
return await tables.rename_table(
|
462
|
+
table_bucket_arn=table_bucket_arn,
|
463
|
+
namespace=namespace,
|
464
|
+
name=name,
|
465
|
+
new_name=new_name,
|
466
|
+
new_namespace_name=new_namespace_name,
|
467
|
+
version_token=version_token,
|
468
|
+
region_name=region_name,
|
469
|
+
)
|
470
|
+
|
471
|
+
|
472
|
+
@app.tool()
|
473
|
+
@log_tool_call_with_response
|
474
|
+
@write_operation
|
475
|
+
async def update_table_metadata_location(
|
476
|
+
table_bucket_arn: Annotated[str, TABLE_BUCKET_ARN_FIELD],
|
477
|
+
namespace: Annotated[str, NAMESPACE_NAME_FIELD],
|
478
|
+
name: Annotated[str, TABLE_NAME_FIELD],
|
479
|
+
metadata_location: Annotated[
|
480
|
+
str,
|
481
|
+
Field(
|
482
|
+
...,
|
483
|
+
description='The new metadata location for the S3 table. Must be 1-2048 characters long.',
|
484
|
+
min_length=1,
|
485
|
+
max_length=2048,
|
486
|
+
),
|
487
|
+
],
|
488
|
+
version_token: Annotated[
|
489
|
+
str,
|
490
|
+
Field(
|
491
|
+
...,
|
492
|
+
description='The version token of the S3 table. Must be 1-2048 characters long.',
|
493
|
+
min_length=1,
|
494
|
+
max_length=2048,
|
495
|
+
),
|
496
|
+
],
|
497
|
+
region_name: Annotated[Optional[str], REGION_NAME_FIELD] = None,
|
498
|
+
):
|
499
|
+
"""Update the metadata location for an S3 table.
|
500
|
+
|
501
|
+
Updates the metadata location for an S3 table. The metadata location of an S3 table must be an S3 URI that begins with the S3 table's warehouse location.
|
502
|
+
The metadata location for an Apache Iceberg S3 table must end with .metadata.json, or if the metadata file is Gzip-compressed, .metadata.json.gz.
|
503
|
+
|
504
|
+
Permissions:
|
505
|
+
You must have the s3tables:UpdateTableMetadataLocation permission to use this operation.
|
506
|
+
"""
|
507
|
+
return await tables.update_table_metadata_location(
|
508
|
+
table_bucket_arn=table_bucket_arn,
|
509
|
+
namespace=namespace,
|
510
|
+
name=name,
|
511
|
+
metadata_location=metadata_location,
|
512
|
+
version_token=version_token,
|
513
|
+
region_name=region_name,
|
514
|
+
)
|
515
|
+
|
516
|
+
|
517
|
+
def _default_uri_for_region(region: str) -> str:
|
518
|
+
return f'https://s3tables.{region}.amazonaws.com/iceberg'
|
519
|
+
|
520
|
+
|
521
|
+
@app.tool()
|
522
|
+
@log_tool_call_with_response
|
523
|
+
async def query_database(
|
524
|
+
warehouse: Annotated[str, Field(..., description='Warehouse string for Iceberg catalog')],
|
525
|
+
region: Annotated[
|
526
|
+
str, Field(..., description='AWS region for S3Tables/Iceberg REST endpoint')
|
527
|
+
],
|
528
|
+
namespace: Annotated[str, NAMESPACE_NAME_FIELD],
|
529
|
+
query: Annotated[str, QUERY_FIELD],
|
530
|
+
uri: Annotated[str, Field(..., description='REST URI for Iceberg catalog')],
|
531
|
+
catalog_name: Annotated[
|
532
|
+
str, Field('s3tablescatalog', description='Catalog name')
|
533
|
+
] = 's3tablescatalog',
|
534
|
+
rest_signing_name: Annotated[
|
535
|
+
str, Field('s3tables', description='REST signing name')
|
536
|
+
] = 's3tables',
|
537
|
+
rest_sigv4_enabled: Annotated[str, Field('true', description='Enable SigV4 signing')] = 'true',
|
538
|
+
):
|
539
|
+
"""Execute SQL queries against S3 Tables using PyIceberg/Daft.
|
540
|
+
|
541
|
+
This tool provides a secure interface to run read-only SQL queries against your S3 Tables data using the PyIceberg and Daft engine.
|
542
|
+
Use a correct region for warehouse, region, and uri.
|
543
|
+
|
544
|
+
Example input values:
|
545
|
+
warehouse: 'arn:aws:s3tables:<Region>:<accountID>:bucket/<bucketname>'
|
546
|
+
region: 'us-west-2'
|
547
|
+
namespace: 'retail_data'
|
548
|
+
query: 'SELECT * FROM customers LIMIT 10'
|
549
|
+
uri: 'https://s3tables.us-west-2.amazonaws.com/iceberg'
|
550
|
+
catalog_name: 's3tablescatalog'
|
551
|
+
rest_signing_name: 's3tables'
|
552
|
+
rest_sigv4_enabled: 'true'
|
553
|
+
"""
|
554
|
+
if uri is None:
|
555
|
+
uri = _default_uri_for_region(region)
|
556
|
+
return await database.query_database_resource(
|
557
|
+
warehouse=warehouse,
|
558
|
+
region=region,
|
559
|
+
namespace=namespace,
|
560
|
+
query=query,
|
561
|
+
uri=uri,
|
562
|
+
catalog_name=catalog_name,
|
563
|
+
rest_signing_name=rest_signing_name,
|
564
|
+
rest_sigv4_enabled=rest_sigv4_enabled,
|
565
|
+
)
|
566
|
+
|
567
|
+
|
568
|
+
@app.tool()
|
569
|
+
@log_tool_call_with_response
|
570
|
+
async def preview_csv_file(
|
571
|
+
s3_url: Annotated[str, S3_URL_FIELD],
|
572
|
+
) -> dict:
|
573
|
+
"""Preview the structure of a CSV file stored in S3.
|
574
|
+
|
575
|
+
This tool provides a quick preview of a CSV file's structure by reading
|
576
|
+
only the headers and first row of data from an S3 location. It's useful for
|
577
|
+
understanding the schema and data format without downloading the entire file.
|
578
|
+
It can be used before creating an s3 table from a csv file to get the schema and data format.
|
579
|
+
|
580
|
+
Returns error dictionary with status and error message if:
|
581
|
+
- URL is not a valid S3 URL
|
582
|
+
- File is not a CSV file
|
583
|
+
- File cannot be accessed
|
584
|
+
- Any other error occurs
|
585
|
+
|
586
|
+
Permissions:
|
587
|
+
You must have the s3:GetObject permission for the S3 bucket and key.
|
588
|
+
"""
|
589
|
+
return file_processor.preview_csv_structure(s3_url)
|
590
|
+
|
591
|
+
|
592
|
+
@app.tool()
|
593
|
+
@log_tool_call_with_response
|
594
|
+
@write_operation
|
595
|
+
async def import_csv_to_table(
|
596
|
+
warehouse: Annotated[str, Field(..., description='Warehouse string for Iceberg catalog')],
|
597
|
+
region: Annotated[
|
598
|
+
str, Field(..., description='AWS region for S3Tables/Iceberg REST endpoint')
|
599
|
+
],
|
600
|
+
namespace: Annotated[str, NAMESPACE_NAME_FIELD],
|
601
|
+
table_name: Annotated[str, TABLE_NAME_FIELD],
|
602
|
+
s3_url: Annotated[str, S3_URL_FIELD],
|
603
|
+
uri: Annotated[str, Field(..., description='REST URI for Iceberg catalog')],
|
604
|
+
catalog_name: Annotated[
|
605
|
+
str, Field('s3tablescatalog', description='Catalog name')
|
606
|
+
] = 's3tablescatalog',
|
607
|
+
rest_signing_name: Annotated[
|
608
|
+
str, Field('s3tables', description='REST signing name')
|
609
|
+
] = 's3tables',
|
610
|
+
rest_sigv4_enabled: Annotated[str, Field('true', description='Enable SigV4 signing')] = 'true',
|
611
|
+
) -> dict:
|
612
|
+
"""Import data from a CSV file into an S3 table.
|
613
|
+
|
614
|
+
This tool reads data from a CSV file stored in S3 and imports it into an existing S3 table.
|
615
|
+
The CSV file must have headers that match the table's schema. The tool will validate the CSV structure
|
616
|
+
before attempting to import the data.
|
617
|
+
|
618
|
+
To create a table, first use the preview_csv_file tool to get the schema and data format.
|
619
|
+
Then use the create_table tool to create the table.
|
620
|
+
|
621
|
+
Returns error dictionary with status and error message if:
|
622
|
+
- URL is not a valid S3 URL
|
623
|
+
- File is not a CSV file
|
624
|
+
- File cannot be accessed
|
625
|
+
- Table does not exist
|
626
|
+
- CSV headers don't match table schema
|
627
|
+
- Any other error occurs
|
628
|
+
|
629
|
+
Example input values:
|
630
|
+
warehouse: 'arn:aws:s3tables:<Region>:<accountID>:bucket/<bucketname>'
|
631
|
+
region: 'us-west-2'
|
632
|
+
namespace: 'retail_data'
|
633
|
+
table_name: 'customers'
|
634
|
+
s3_url: 's3://bucket-name/path/to/file.csv'
|
635
|
+
uri: 'https://s3tables.us-west-2.amazonaws.com/iceberg'
|
636
|
+
catalog_name: 's3tablescatalog'
|
637
|
+
rest_signing_name: 's3tables'
|
638
|
+
rest_sigv4_enabled: 'true'
|
639
|
+
|
640
|
+
Permissions:
|
641
|
+
You must have:
|
642
|
+
- s3:GetObject permission for the CSV file
|
643
|
+
- s3tables:GetDatabase and s3tables:GetDatabases permissions to access database information
|
644
|
+
- s3tables:GetTable and s3tables:GetTables permissions to access table information
|
645
|
+
- s3tables:PutTableData permission to write to the table
|
646
|
+
"""
|
647
|
+
if uri is None:
|
648
|
+
uri = _default_uri_for_region(region)
|
649
|
+
return await file_processor.import_csv_to_table(
|
650
|
+
warehouse=warehouse,
|
651
|
+
region=region,
|
652
|
+
namespace=namespace,
|
653
|
+
table_name=table_name,
|
654
|
+
s3_url=s3_url,
|
655
|
+
uri=uri,
|
656
|
+
catalog_name=catalog_name,
|
657
|
+
rest_signing_name=rest_signing_name,
|
658
|
+
rest_sigv4_enabled=rest_sigv4_enabled,
|
659
|
+
)
|
660
|
+
|
661
|
+
|
662
|
+
@app.tool()
|
663
|
+
@log_tool_call_with_response
|
664
|
+
async def get_bucket_metadata_config(
|
665
|
+
bucket: Annotated[
|
666
|
+
str,
|
667
|
+
Field(
|
668
|
+
...,
|
669
|
+
description='The name of the S3 bucket to get metadata table configuration for.',
|
670
|
+
min_length=1,
|
671
|
+
),
|
672
|
+
],
|
673
|
+
region_name: Annotated[Optional[str], REGION_NAME_FIELD] = None,
|
674
|
+
) -> dict:
|
675
|
+
"""Get the metadata table configuration for a regular general purpose S3 bucket.
|
676
|
+
|
677
|
+
Retrieves the metadata table configuration for a regular general purpose bucket in s3. This configuration
|
678
|
+
determines how metadata is stored and managed for the bucket.
|
679
|
+
The response includes:
|
680
|
+
- S3 Table Bucket ARN
|
681
|
+
- S3 Table ARN
|
682
|
+
- S3 Table Name
|
683
|
+
- S3 Table Namespace
|
684
|
+
|
685
|
+
Description:
|
686
|
+
Amazon S3 Metadata accelerates data discovery by automatically capturing metadata for the objects in your general purpose buckets and storing it in read-only, fully managed Apache Iceberg tables that you can query. These read-only tables are called metadata tables. As objects are added to, updated, and removed from your general purpose buckets, S3 Metadata automatically refreshes the corresponding metadata tables to reflect the latest changes.
|
687
|
+
By default, S3 Metadata provides three types of metadata:
|
688
|
+
- System-defined metadata, such as an object's creation time and storage class
|
689
|
+
- Custom metadata, such as tags and user-defined metadata that was included during object upload
|
690
|
+
- Event metadata, such as when an object is updated or deleted, and the AWS account that made the request
|
691
|
+
|
692
|
+
Metadata table schema:
|
693
|
+
- bucket: String
|
694
|
+
- key: String
|
695
|
+
- sequence_number: String
|
696
|
+
- record_type: String
|
697
|
+
- record_timestamp: Timestamp (no time zone)
|
698
|
+
- version_id: String
|
699
|
+
- is_delete_marker: Boolean
|
700
|
+
- size: Long
|
701
|
+
- last_modified_date: Timestamp (no time zone)
|
702
|
+
- e_tag: String
|
703
|
+
- storage_class: String
|
704
|
+
- is_multipart: Boolean
|
705
|
+
- encryption_status: String
|
706
|
+
- is_bucket_key_enabled: Boolean
|
707
|
+
- kms_key_arn: String
|
708
|
+
- checksum_algorithm: String
|
709
|
+
- object_tags: Map<String, String>
|
710
|
+
- user_metadata: Map<String, String>
|
711
|
+
- requester: String
|
712
|
+
- source_ip_address: String
|
713
|
+
- request_id: String
|
714
|
+
|
715
|
+
Permissions:
|
716
|
+
You must have the s3:GetBucketMetadataTableConfiguration permission to use this operation.
|
717
|
+
"""
|
718
|
+
return await s3_operations.get_bucket_metadata_table_configuration(
|
719
|
+
bucket=bucket, region_name=region_name
|
720
|
+
)
|
721
|
+
|
722
|
+
|
723
|
+
@app.tool()
|
724
|
+
@log_tool_call_with_response
|
725
|
+
@write_operation
|
726
|
+
async def append_rows_to_table(
|
727
|
+
warehouse: Annotated[str, Field(..., description='Warehouse string for Iceberg catalog')],
|
728
|
+
region: Annotated[
|
729
|
+
str, Field(..., description='AWS region for S3Tables/Iceberg REST endpoint')
|
730
|
+
],
|
731
|
+
namespace: Annotated[str, NAMESPACE_NAME_FIELD],
|
732
|
+
table_name: Annotated[str, TABLE_NAME_FIELD],
|
733
|
+
rows: Annotated[list[dict], Field(..., description='List of rows to append, each as a dict')],
|
734
|
+
uri: Annotated[str, Field(..., description='REST URI for Iceberg catalog')],
|
735
|
+
catalog_name: Annotated[
|
736
|
+
str, Field('s3tablescatalog', description='Catalog name')
|
737
|
+
] = 's3tablescatalog',
|
738
|
+
rest_signing_name: Annotated[
|
739
|
+
str, Field('s3tables', description='REST signing name')
|
740
|
+
] = 's3tables',
|
741
|
+
rest_sigv4_enabled: Annotated[str, Field('true', description='Enable SigV4 signing')] = 'true',
|
742
|
+
) -> dict:
|
743
|
+
"""Append rows to an Iceberg table using PyIceberg/Daft.
|
744
|
+
|
745
|
+
This tool appends data rows to an existing Iceberg table using the PyIceberg engine.
|
746
|
+
The rows parameter must be a list of dictionaries, each representing a row.
|
747
|
+
Check the schema of the table before appending rows.
|
748
|
+
|
749
|
+
Example input values:
|
750
|
+
warehouse: 'arn:aws:s3tables:<Region>:<accountID>:bucket/<bucketname>'
|
751
|
+
region: 'us-west-2'
|
752
|
+
namespace: 'retail_data'
|
753
|
+
table_name: 'customers'
|
754
|
+
rows: [{"customer_id": 1, "customer_name": "Alice"}, ...]
|
755
|
+
uri: 'https://s3tables.us-west-2.amazonaws.com/iceberg'
|
756
|
+
catalog_name: 's3tablescatalog'
|
757
|
+
rest_signing_name: 's3tables'
|
758
|
+
rest_sigv4_enabled: 'true'
|
759
|
+
"""
|
760
|
+
if uri is None:
|
761
|
+
uri = _default_uri_for_region(region)
|
762
|
+
return await database.append_rows_to_table_resource(
|
763
|
+
warehouse=warehouse,
|
764
|
+
region=region,
|
765
|
+
namespace=namespace,
|
766
|
+
table_name=table_name,
|
767
|
+
rows=rows,
|
768
|
+
uri=uri,
|
769
|
+
catalog_name=catalog_name,
|
770
|
+
rest_signing_name=rest_signing_name,
|
771
|
+
rest_sigv4_enabled=rest_sigv4_enabled,
|
772
|
+
)
|
773
|
+
|
774
|
+
|
775
|
+
def main():
|
776
|
+
"""Run the MCP server with CLI argument support.
|
777
|
+
|
778
|
+
This function initializes and runs the AWS S3 Tables MCP server, which provides
|
779
|
+
programmatic access to manage S3 tables through the Model Context Protocol.
|
780
|
+
"""
|
781
|
+
parser = argparse.ArgumentParser(
|
782
|
+
description='An AWS Labs Model Context Protocol (MCP) server for S3 Tables'
|
783
|
+
)
|
784
|
+
parser.add_argument(
|
785
|
+
'--allow-write',
|
786
|
+
action='store_true',
|
787
|
+
help='Allow write operations. By default, the server runs in read-only mode.',
|
788
|
+
)
|
789
|
+
parser.add_argument(
|
790
|
+
'--log-dir',
|
791
|
+
type=str,
|
792
|
+
default=None,
|
793
|
+
help='Directory to write logs to. Defaults to /var/logs on Linux and ~/Library/Logs on MacOS.',
|
794
|
+
)
|
795
|
+
|
796
|
+
args = parser.parse_args()
|
797
|
+
|
798
|
+
app.allow_write = args.allow_write
|
799
|
+
set_user_agent_mode(args.allow_write)
|
800
|
+
|
801
|
+
# Determine log directory
|
802
|
+
if args.log_dir:
|
803
|
+
app.log_dir = os.path.expanduser(args.log_dir)
|
804
|
+
|
805
|
+
# Log program startup details
|
806
|
+
log_tool_call(
|
807
|
+
'server_start',
|
808
|
+
argv=sys.argv,
|
809
|
+
parsed_args=vars(args),
|
810
|
+
mcp_version=__version__,
|
811
|
+
python_version=sys.version,
|
812
|
+
platform=platform.platform(),
|
813
|
+
)
|
814
|
+
|
815
|
+
app.run()
|
816
|
+
|
817
|
+
|
818
|
+
# FastMCP application runner
|
819
|
+
if __name__ == '__main__':
|
820
|
+
print('Starting S3 Tables MCP server...')
|
821
|
+
main()
|