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.
Files changed (23) hide show
  1. {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/PKG-INFO +1 -1
  2. {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/awslabs/redshift_mcp_server/__init__.py +1 -1
  3. {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/awslabs/redshift_mcp_server/redshift.py +40 -17
  4. {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/pyproject.toml +1 -1
  5. {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/tests/test_redshift.py +118 -0
  6. awslabs_redshift_mcp_server-0.0.12/uv-requirements.txt +27 -0
  7. {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/uv.lock +1 -1
  8. awslabs_redshift_mcp_server-0.0.10/uv-requirements.txt +0 -24
  9. {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/.gitignore +0 -0
  10. {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/.python-version +0 -0
  11. {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/CHANGELOG.md +0 -0
  12. {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/Dockerfile +0 -0
  13. {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/LICENSE +0 -0
  14. {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/NOTICE +0 -0
  15. {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/README.md +0 -0
  16. {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/awslabs/__init__.py +0 -0
  17. {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/awslabs/redshift_mcp_server/consts.py +0 -0
  18. {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/awslabs/redshift_mcp_server/models.py +0 -0
  19. {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/awslabs/redshift_mcp_server/server.py +0 -0
  20. {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/docker-healthcheck.sh +0 -0
  21. {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/tests/test_init.py +0 -0
  22. {awslabs_redshift_mcp_server-0.0.10 → awslabs_redshift_mcp_server-0.0.12}/tests/test_main.py +0 -0
  23. {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.10
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/
@@ -14,4 +14,4 @@
14
14
 
15
15
  """awslabs.redshift-mcp-server"""
16
16
 
17
- __version__ = '0.0.10'
17
+ __version__ = '0.0.12'
@@ -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 = await _execute_statement(
268
- cluster_info=cluster_info,
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
- # Execute END statement to close transaction
277
- await _execute_statement(
278
- cluster_info=cluster_info,
279
- cluster_identifier=cluster_identifier,
280
- database_name=database_name,
281
- sql='END;',
282
- session_id=session_id,
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
 
@@ -2,7 +2,7 @@
2
2
  name = "awslabs.redshift-mcp-server"
3
3
 
4
4
  # NOTE: "Patch"=9223372036854775807 bumps next release to zero.
5
- version = "0.0.10"
5
+ version = "0.0.12"
6
6
 
7
7
  description = "An AWS Labs Model Context Protocol (MCP) server for Redshift"
8
8
  readme = "README.md"
@@ -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`)
@@ -46,7 +46,7 @@ wheels = [
46
46
 
47
47
  [[package]]
48
48
  name = "awslabs-redshift-mcp-server"
49
- version = "0.0.10"
49
+ version = "0.0.12"
50
50
  source = { editable = "." }
51
51
  dependencies = [
52
52
  { name = "boto3" },
@@ -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