awslabs.redshift-mcp-server 0.0.10__tar.gz → 0.0.12__tar.gz
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_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/PKG-INFO +1 -1
- {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/awslabs/redshift_mcp_server/__init__.py +1 -1
- {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/awslabs/redshift_mcp_server/redshift.py +40 -17
- {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/pyproject.toml +1 -1
- {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/tests/test_redshift.py +118 -0
- awslabs_redshift_mcp_server-0.0.12/uv-requirements.txt +27 -0
- {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/uv.lock +1 -1
- awslabs_redshift_mcp_server-0.0.10/uv-requirements.txt +0 -24
- {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/.gitignore +0 -0
- {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/.python-version +0 -0
- {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/CHANGELOG.md +0 -0
- {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/Dockerfile +0 -0
- {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/LICENSE +0 -0
- {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/NOTICE +0 -0
- {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/README.md +0 -0
- {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/awslabs/__init__.py +0 -0
- {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/awslabs/redshift_mcp_server/consts.py +0 -0
- {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/awslabs/redshift_mcp_server/models.py +0 -0
- {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/awslabs/redshift_mcp_server/server.py +0 -0
- {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/docker-healthcheck.sh +0 -0
- {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/tests/test_init.py +0 -0
- {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/tests/test_main.py +0 -0
- {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/tests/test_server.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: awslabs.redshift-mcp-server
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.12
|
|
4
4
|
Summary: An AWS Labs Model Context Protocol (MCP) server for Redshift
|
|
5
5
|
Project-URL: homepage, https://awslabs.github.io/mcp/
|
|
6
6
|
Project-URL: docs, https://awslabs.github.io/mcp/servers/redshift-mcp-server/
|
|
@@ -263,27 +263,50 @@ async def _execute_protected_statement(
|
|
|
263
263
|
session_id=session_id,
|
|
264
264
|
)
|
|
265
265
|
|
|
266
|
-
# Execute user SQL with parameters
|
|
267
|
-
user_query_id =
|
|
268
|
-
|
|
269
|
-
cluster_identifier=cluster_identifier,
|
|
270
|
-
database_name=database_name,
|
|
271
|
-
sql=sql,
|
|
272
|
-
parameters=parameters,
|
|
273
|
-
session_id=session_id,
|
|
274
|
-
)
|
|
266
|
+
# Execute user SQL with parameters, ensuring transaction is always closed
|
|
267
|
+
user_query_id = None
|
|
268
|
+
user_sql_error = None
|
|
275
269
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
270
|
+
try:
|
|
271
|
+
user_query_id = await _execute_statement(
|
|
272
|
+
cluster_info=cluster_info,
|
|
273
|
+
cluster_identifier=cluster_identifier,
|
|
274
|
+
database_name=database_name,
|
|
275
|
+
sql=sql,
|
|
276
|
+
parameters=parameters,
|
|
277
|
+
session_id=session_id,
|
|
278
|
+
)
|
|
279
|
+
except Exception as e:
|
|
280
|
+
user_sql_error = e
|
|
281
|
+
logger.error(f'User SQL execution failed: {e}')
|
|
282
|
+
|
|
283
|
+
# Always execute END statement to close transaction
|
|
284
|
+
try:
|
|
285
|
+
await _execute_statement(
|
|
286
|
+
cluster_info=cluster_info,
|
|
287
|
+
cluster_identifier=cluster_identifier,
|
|
288
|
+
database_name=database_name,
|
|
289
|
+
sql='END;',
|
|
290
|
+
session_id=session_id,
|
|
291
|
+
)
|
|
292
|
+
except Exception as end_error:
|
|
293
|
+
logger.error(f'END statement execution failed: {end_error}')
|
|
294
|
+
if user_sql_error:
|
|
295
|
+
# Both failed - raise combined error
|
|
296
|
+
raise Exception(
|
|
297
|
+
f'User SQL failed: {user_sql_error}; END statement failed: {end_error}'
|
|
298
|
+
)
|
|
299
|
+
else:
|
|
300
|
+
# Only END failed
|
|
301
|
+
raise end_error
|
|
302
|
+
|
|
303
|
+
# If user SQL failed but END succeeded, raise user SQL error
|
|
304
|
+
if user_sql_error:
|
|
305
|
+
raise user_sql_error
|
|
284
306
|
|
|
285
307
|
# Get results from user query
|
|
286
308
|
data_client = client_manager.redshift_data_client()
|
|
309
|
+
assert user_query_id is not None, 'user_query_id should not be None at this point'
|
|
287
310
|
results_response = data_client.get_statement_result(Id=user_query_id)
|
|
288
311
|
return results_response, user_query_id
|
|
289
312
|
|
{awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/tests/test_redshift.py
RENAMED
|
@@ -391,6 +391,124 @@ class TestExecuteProtectedStatement:
|
|
|
391
391
|
'target-cluster', 'test-db', 'SELECT 1', allow_read_write=False
|
|
392
392
|
)
|
|
393
393
|
|
|
394
|
+
@pytest.mark.asyncio
|
|
395
|
+
async def test_execute_protected_statement_user_sql_fails_end_succeeds(self, mocker):
|
|
396
|
+
"""Test user SQL fails but END succeeds - should raise user SQL error."""
|
|
397
|
+
# Mock discover_clusters
|
|
398
|
+
mock_discover_clusters = mocker.patch(
|
|
399
|
+
'awslabs.redshift_mcp_server.redshift.discover_clusters'
|
|
400
|
+
)
|
|
401
|
+
mock_discover_clusters.return_value = [
|
|
402
|
+
{'identifier': 'test-cluster', 'type': 'provisioned'}
|
|
403
|
+
]
|
|
404
|
+
|
|
405
|
+
# Mock session manager
|
|
406
|
+
mock_session_manager = mocker.patch('awslabs.redshift_mcp_server.redshift.session_manager')
|
|
407
|
+
mock_session_manager.session = mocker.AsyncMock(return_value='session-123')
|
|
408
|
+
|
|
409
|
+
# Mock _execute_statement to fail for user SQL, succeed for BEGIN and END
|
|
410
|
+
mock_execute_statement = mocker.patch(
|
|
411
|
+
'awslabs.redshift_mcp_server.redshift._execute_statement'
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
def execute_side_effect(cluster_info, cluster_identifier, database_name, sql, **kwargs):
|
|
415
|
+
if sql == 'BEGIN READ ONLY;':
|
|
416
|
+
return 'begin-stmt-id'
|
|
417
|
+
elif sql == 'SELECT invalid_syntax':
|
|
418
|
+
raise Exception('SQL syntax error')
|
|
419
|
+
elif sql == 'END;':
|
|
420
|
+
return 'end-stmt-id'
|
|
421
|
+
return 'stmt-id'
|
|
422
|
+
|
|
423
|
+
mock_execute_statement.side_effect = execute_side_effect
|
|
424
|
+
|
|
425
|
+
with pytest.raises(Exception, match='SQL syntax error'):
|
|
426
|
+
await _execute_protected_statement(
|
|
427
|
+
'test-cluster', 'test-db', 'SELECT invalid_syntax', allow_read_write=False
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
# Verify END was still called
|
|
431
|
+
assert mock_execute_statement.call_count == 3
|
|
432
|
+
calls = mock_execute_statement.call_args_list
|
|
433
|
+
assert calls[0][1]['sql'] == 'BEGIN READ ONLY;'
|
|
434
|
+
assert calls[1][1]['sql'] == 'SELECT invalid_syntax'
|
|
435
|
+
assert calls[2][1]['sql'] == 'END;'
|
|
436
|
+
|
|
437
|
+
@pytest.mark.asyncio
|
|
438
|
+
async def test_execute_protected_statement_user_sql_succeeds_end_fails(self, mocker):
|
|
439
|
+
"""Test user SQL succeeds but END fails - should raise END error."""
|
|
440
|
+
# Mock discover_clusters
|
|
441
|
+
mock_discover_clusters = mocker.patch(
|
|
442
|
+
'awslabs.redshift_mcp_server.redshift.discover_clusters'
|
|
443
|
+
)
|
|
444
|
+
mock_discover_clusters.return_value = [
|
|
445
|
+
{'identifier': 'test-cluster', 'type': 'provisioned'}
|
|
446
|
+
]
|
|
447
|
+
|
|
448
|
+
# Mock session manager
|
|
449
|
+
mock_session_manager = mocker.patch('awslabs.redshift_mcp_server.redshift.session_manager')
|
|
450
|
+
mock_session_manager.session = mocker.AsyncMock(return_value='session-123')
|
|
451
|
+
|
|
452
|
+
# Mock _execute_statement to succeed for user SQL, fail for END
|
|
453
|
+
mock_execute_statement = mocker.patch(
|
|
454
|
+
'awslabs.redshift_mcp_server.redshift._execute_statement'
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
def execute_side_effect(cluster_info, cluster_identifier, database_name, sql, **kwargs):
|
|
458
|
+
if sql == 'BEGIN READ ONLY;':
|
|
459
|
+
return 'begin-stmt-id'
|
|
460
|
+
elif sql == 'SELECT 1':
|
|
461
|
+
return 'user-stmt-id'
|
|
462
|
+
elif sql == 'END;':
|
|
463
|
+
raise Exception('END statement failed')
|
|
464
|
+
return 'stmt-id'
|
|
465
|
+
|
|
466
|
+
mock_execute_statement.side_effect = execute_side_effect
|
|
467
|
+
|
|
468
|
+
with pytest.raises(Exception, match='END statement failed'):
|
|
469
|
+
await _execute_protected_statement(
|
|
470
|
+
'test-cluster', 'test-db', 'SELECT 1', allow_read_write=False
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
@pytest.mark.asyncio
|
|
474
|
+
async def test_execute_protected_statement_both_user_sql_and_end_fail(self, mocker):
|
|
475
|
+
"""Test both user SQL and END fail - should raise combined error."""
|
|
476
|
+
# Mock discover_clusters
|
|
477
|
+
mock_discover_clusters = mocker.patch(
|
|
478
|
+
'awslabs.redshift_mcp_server.redshift.discover_clusters'
|
|
479
|
+
)
|
|
480
|
+
mock_discover_clusters.return_value = [
|
|
481
|
+
{'identifier': 'test-cluster', 'type': 'provisioned'}
|
|
482
|
+
]
|
|
483
|
+
|
|
484
|
+
# Mock session manager
|
|
485
|
+
mock_session_manager = mocker.patch('awslabs.redshift_mcp_server.redshift.session_manager')
|
|
486
|
+
mock_session_manager.session = mocker.AsyncMock(return_value='session-123')
|
|
487
|
+
|
|
488
|
+
# Mock _execute_statement to fail for both user SQL and END
|
|
489
|
+
mock_execute_statement = mocker.patch(
|
|
490
|
+
'awslabs.redshift_mcp_server.redshift._execute_statement'
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
def execute_side_effect(cluster_info, cluster_identifier, database_name, sql, **kwargs):
|
|
494
|
+
if sql == 'BEGIN READ ONLY;':
|
|
495
|
+
return 'begin-stmt-id'
|
|
496
|
+
elif sql == 'SELECT invalid_syntax':
|
|
497
|
+
raise Exception('SQL syntax error')
|
|
498
|
+
elif sql == 'END;':
|
|
499
|
+
raise Exception('END statement failed')
|
|
500
|
+
return 'stmt-id'
|
|
501
|
+
|
|
502
|
+
mock_execute_statement.side_effect = execute_side_effect
|
|
503
|
+
|
|
504
|
+
with pytest.raises(
|
|
505
|
+
Exception,
|
|
506
|
+
match='User SQL failed: SQL syntax error; END statement failed: END statement failed',
|
|
507
|
+
):
|
|
508
|
+
await _execute_protected_statement(
|
|
509
|
+
'test-cluster', 'test-db', 'SELECT invalid_syntax', allow_read_write=False
|
|
510
|
+
)
|
|
511
|
+
|
|
394
512
|
|
|
395
513
|
class TestExecuteStatement:
|
|
396
514
|
"""Tests for _execute_statement function."""
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#
|
|
2
|
+
# This file is autogenerated by pip-compile with Python 3.12
|
|
3
|
+
# by the following command:
|
|
4
|
+
#
|
|
5
|
+
# pip-compile --generate-hashes --output-file=uv-requirements.txt --strip-extras uv-requirements.in
|
|
6
|
+
#
|
|
7
|
+
uv==0.9.6 \
|
|
8
|
+
--hash=sha256:0169a85d3ba5ef1c37089d64ff26de573439ca84ecf549276a2eee42d7f833f2 \
|
|
9
|
+
--hash=sha256:0fde18c22376c8b02954c7db3847bc75ac42619932c44b43f49d056e5cfb05f9 \
|
|
10
|
+
--hash=sha256:166175ba952d2ad727e1dbd57d7cfc1782dfe7b8d79972174a46a7aa33ddceec \
|
|
11
|
+
--hash=sha256:3c2c2b2b093330e603d838fec26941ab6f62e8d62a012f9fa0d5ed88da39d907 \
|
|
12
|
+
--hash=sha256:538716ec97f8d899baa7e1c427f4411525459c0ef72ea9b3625ce9610c9976e6 \
|
|
13
|
+
--hash=sha256:547fd27ab5da7cd1a833288a36858852451d416a056825f162ecf2af5be6f8b8 \
|
|
14
|
+
--hash=sha256:62e3f057a9ae5e5003a7cd56b617e940f519f6dabcbb22d36cdd0149df25d409 \
|
|
15
|
+
--hash=sha256:6403176b55388cf94fb8737e73b26ee2a7b1805a9139da5afa951210986d4fcd \
|
|
16
|
+
--hash=sha256:7e89c964f614fa3f0481060cac709d6da50feac553e1e11227d6c4c81c87af7c \
|
|
17
|
+
--hash=sha256:86e05782f9b75d39ab1c0af98bf11e87e646a36a61d425021d5b284073e56315 \
|
|
18
|
+
--hash=sha256:90122a76e6441b8c580fc9faf06bd8c4dbe276cb1c185ad91eceb2afa78e492a \
|
|
19
|
+
--hash=sha256:95a62c1f668272555ad0c446bf44a9924dee06054b831d04c162e0bad736dc28 \
|
|
20
|
+
--hash=sha256:a7c6067919d87208c4a6092033c3bc9799cb8be1c8bc6ef419a1f6d42a755329 \
|
|
21
|
+
--hash=sha256:b2f934737c93f88c906b6a47bcc083170210fe5d66565e80a7c139599e5cbf2f \
|
|
22
|
+
--hash=sha256:b31377ebf2d0499afc5abe3fe1abded5ca843f3a1161b432fe26eb0ce15bab8e \
|
|
23
|
+
--hash=sha256:d1072db92cc9525febdf9d113c23916dfc20ca03e21218cc7beefe7185a90631 \
|
|
24
|
+
--hash=sha256:e700b2098f9d365061c572d0729b4e8bc71c6468d83dfaae2537cd66e3cb1b98 \
|
|
25
|
+
--hash=sha256:ea67369918af24ea7e01991dfc8b8988d1b0b7c49cb39d9e5bc0c409930a0a3f \
|
|
26
|
+
--hash=sha256:f0ba311b3ca49d246f36d444d3ee81571619ef95e5f509eb694a81defcbed262
|
|
27
|
+
# via -r uv-requirements.in (contents of `uv==0.9.6`)
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# This file was autogenerated by uv via the following command:
|
|
2
|
-
# echo "uv==0.8.10" > uv-requirements.in
|
|
3
|
-
# uv pip compile --generate-hashes --output-file=uv-requirements.txt --strip-extras --python=3.10 uv-requirements.in
|
|
4
|
-
uv==0.8.10 \
|
|
5
|
-
--hash=sha256:31e4fc37ee94b94c032384a0957ad32ba7dce4ce6c04b4880fd3e31e25e51a82 \
|
|
6
|
-
--hash=sha256:36a5ce708d52388c37043e7335f9eb3fea5a19a56166a2cc6adb365179a1cd77 \
|
|
7
|
-
--hash=sha256:38286d230daad82388469c8dc7a1d2f5dc279c11178319c886d1a88d7938e513 \
|
|
8
|
-
--hash=sha256:3e190cee3bb2b4f574a419eef87ae8e33f713e9cd6f856b83277ece70ad9ca9b \
|
|
9
|
-
--hash=sha256:3fdf89fc40af9902141c39ed943bcfca15664623363335eb032a44f22001e2b4 \
|
|
10
|
-
--hash=sha256:4cc190d403a89e46d13cec83b6f8e8d7d07aaf1e5a996eac9a3f0c2a8cd92537 \
|
|
11
|
-
--hash=sha256:57b71dc79eff25a5419d3fe4a563d3b9397f55d789f685ef27f43f033b31f482 \
|
|
12
|
-
--hash=sha256:86fe044c2be43977566a0d184a487edd7aace2febb757fd95927684b629ef50b \
|
|
13
|
-
--hash=sha256:88df34c32555064fae459cce665757619fd1af7deb2dc393352b15d909d2d131 \
|
|
14
|
-
--hash=sha256:9ad21eeaa4156a1bf5ed85903f80db06e2c02badd3a587ba98d3171517960555 \
|
|
15
|
-
--hash=sha256:a5495b5a6e3111c03cf5e4dbdd598bc8fd1da887e3920d58cd5a2d4c8bc9a473 \
|
|
16
|
-
--hash=sha256:ab072cd3bf2f9dc264659a1ff48ad91a910ac4830bcfe965e2d3f89c86646f46 \
|
|
17
|
-
--hash=sha256:af8a5526b0e331775a264fa0dbccfd53c183cb974f269a208af136d7561f9eb2 \
|
|
18
|
-
--hash=sha256:b00637c63d5dfc9f879281c5c91db2bb909ab1f9ab275dab015e7fb6cac6be5b \
|
|
19
|
-
--hash=sha256:b3ff3c451fcd23ea78356d8c18e802d0e423cbe655273601e3ec039a51b33286 \
|
|
20
|
-
--hash=sha256:c4a493cd4b15b3aef11523531aff96a77a586666a63e842fa437966b7b7ee62d \
|
|
21
|
-
--hash=sha256:defc50bb319be2d58be74a680710cd4b7697e88d5f79974eacd354df95f0b6b0 \
|
|
22
|
-
--hash=sha256:e0a02bcec766eb0862b7082ab746b204add7d9fcaa62322502d159b5a7ccc54a \
|
|
23
|
-
--hash=sha256:eb79a46d8099f563ef58237bf4e9009f876a40145e757ea883a92b24b724d01e
|
|
24
|
-
# via -r uv-requirements.in
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/awslabs/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/docker-healthcheck.sh
RENAMED
|
File without changes
|
{awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/tests/test_init.py
RENAMED
|
File without changes
|
{awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/tests/test_main.py
RENAMED
|
File without changes
|
{awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/tests/test_server.py
RENAMED
|
File without changes
|