awslabs.valkey-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 ADDED
@@ -0,0 +1,13 @@
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
+ # This file is part of the awslabs namespace.
13
+ # It is intentionally minimal to support PEP 420 namespace packages.
@@ -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
+ """Valkey MCP server package."""
13
+
14
+ __version__ = '0.1.0'
@@ -0,0 +1,20 @@
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
+ Common imports.
14
+ """
15
+
16
+ from . import (
17
+ config,
18
+ connection,
19
+ server,
20
+ )
@@ -0,0 +1,85 @@
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
+ import os
13
+ import urllib.parse
14
+ from dotenv import load_dotenv
15
+
16
+
17
+ load_dotenv()
18
+
19
+ MCP_TRANSPORT = os.getenv('MCP_TRANSPORT', 'stdio')
20
+
21
+ VALKEY_CFG = {
22
+ 'host': os.getenv('VALKEY_HOST', '127.0.0.1'),
23
+ 'port': int(os.getenv('VALKEY_PORT', 6379)),
24
+ 'username': os.getenv('VALKEY_USERNAME', None),
25
+ 'password': os.getenv('VALKEY_PWD', ''),
26
+ 'ssl': os.getenv('VALKEY_USE_SSL', False) in ('true', '1', 't'),
27
+ 'ssl_ca_path': os.getenv('VALKEY_SSL_CA_PATH', None),
28
+ 'ssl_keyfile': os.getenv('VALKEY_SSL_KEYFILE', None),
29
+ 'ssl_certfile': os.getenv('VALKEY_SSL_CERTFILE', None),
30
+ 'ssl_cert_reqs': os.getenv('VALKEY_SSL_CERT_REQS', 'required'),
31
+ 'ssl_ca_certs': os.getenv('VALKEY_SSL_CA_CERTS', None),
32
+ 'cluster_mode': os.getenv('VALKEY_CLUSTER_MODE', False) in ('true', '1', 't'),
33
+ }
34
+
35
+
36
+ def generate_valkey_uri():
37
+ """Generates Valkey URL."""
38
+ cfg = VALKEY_CFG
39
+ scheme = 'valkeys' if cfg.get('ssl') else 'valkey'
40
+ host = cfg.get('host', '127.0.0.1')
41
+ port = cfg.get('port', 6379)
42
+
43
+ username = cfg.get('username')
44
+ password = cfg.get('password')
45
+
46
+ # Auth part - use quote() for auth components to preserve spaces as %20
47
+ def safe_quote(value):
48
+ """Safely quote a value that might be None."""
49
+ if value is None:
50
+ return ''
51
+ return urllib.parse.quote(str(value))
52
+
53
+ if username:
54
+ auth_part = f'{safe_quote(username)}:{safe_quote(password)}@'
55
+ elif password:
56
+ auth_part = f':{safe_quote(password)}@'
57
+ else:
58
+ auth_part = ''
59
+
60
+ # Base URI
61
+ base_uri = f'{scheme}://{auth_part}{host}:{port}'
62
+
63
+ # Additional SSL query parameters if SSL is enabled
64
+ query_params = {}
65
+ if cfg.get('ssl'):
66
+ if cfg.get('ssl_cert_reqs'):
67
+ query_params['ssl_cert_reqs'] = cfg['ssl_cert_reqs']
68
+ if cfg.get('ssl_ca_certs'):
69
+ query_params['ssl_ca_certs'] = cfg['ssl_ca_certs']
70
+ if cfg.get('ssl_keyfile'):
71
+ query_params['ssl_keyfile'] = cfg['ssl_keyfile']
72
+ if cfg.get('ssl_certfile'):
73
+ query_params['ssl_certfile'] = cfg['ssl_certfile']
74
+ if cfg.get('ssl_ca_path'):
75
+ query_params['ssl_ca_path'] = cfg['ssl_ca_path']
76
+
77
+ if query_params:
78
+ # Build query string with proper URL encoding
79
+ query_parts = []
80
+ for key, value in sorted(query_params.items()):
81
+ encoded_value = urllib.parse.quote(str(value), safe='')
82
+ query_parts.append(f'{key}={encoded_value}')
83
+ base_uri += '?' + '&'.join(query_parts)
84
+
85
+ return base_uri
@@ -0,0 +1,97 @@
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
+ import sys
13
+ from awslabs.valkey_mcp_server.common.config import VALKEY_CFG
14
+ from awslabs.valkey_mcp_server.version import __version__
15
+ from typing import Optional, Type, Union
16
+ from valkey import (
17
+ Valkey,
18
+ exceptions,
19
+ )
20
+ from valkey.cluster import ValkeyCluster
21
+
22
+
23
+ class ValkeyConnectionManager:
24
+ """Manages connection to Valkey."""
25
+
26
+ _instance: Optional[Union[Valkey, ValkeyCluster]] = None
27
+
28
+ @classmethod
29
+ def get_connection(cls, decode_responses: bool = True) -> Union[Valkey, ValkeyCluster]:
30
+ """Create connection to Valkey if none present or returns existing connection.
31
+
32
+ Args:
33
+ decode_responses: Whether to decode response bytes to strings. Defaults to True.
34
+
35
+ Returns:
36
+ Valkey: A Valkey connection instance.
37
+ """
38
+ if cls._instance is None:
39
+ try:
40
+ valkey_class: Type[Union[Valkey, ValkeyCluster]] = (
41
+ ValkeyCluster if VALKEY_CFG['cluster_mode'] else Valkey
42
+ )
43
+
44
+ # Get SSL settings with defaults
45
+ ssl_enabled = VALKEY_CFG.get('ssl', False)
46
+ ssl_cert_reqs = VALKEY_CFG.get('ssl_cert_reqs')
47
+ if ssl_enabled and ssl_cert_reqs is None:
48
+ ssl_cert_reqs = 'required'
49
+
50
+ # Build connection kwargs
51
+ connection_kwargs = {
52
+ 'host': VALKEY_CFG['host'],
53
+ 'port': VALKEY_CFG['port'],
54
+ 'username': VALKEY_CFG.get('username'),
55
+ 'password': VALKEY_CFG.get('password', ''),
56
+ 'ssl': ssl_enabled,
57
+ 'ssl_ca_path': VALKEY_CFG.get('ssl_ca_path'),
58
+ 'ssl_keyfile': VALKEY_CFG.get('ssl_keyfile'),
59
+ 'ssl_certfile': VALKEY_CFG.get('ssl_certfile'),
60
+ 'ssl_cert_reqs': ssl_cert_reqs,
61
+ 'ssl_ca_certs': VALKEY_CFG.get('ssl_ca_certs'),
62
+ 'decode_responses': decode_responses,
63
+ 'lib_name': f'valkey-py(mcp-server_v{__version__})',
64
+ }
65
+
66
+ # Add max_connections parameter based on mode
67
+ if VALKEY_CFG['cluster_mode']:
68
+ connection_kwargs['max_connections_per_node'] = 10
69
+ else:
70
+ connection_kwargs['max_connections'] = 10
71
+
72
+ # Create new instance
73
+ cls._instance = valkey_class(**connection_kwargs)
74
+
75
+ except exceptions.AuthenticationError:
76
+ print('Authentication failed', file=sys.stderr)
77
+ raise
78
+ except exceptions.ConnectionError:
79
+ print('Failed to connect to Valkey server', file=sys.stderr)
80
+ raise
81
+ except exceptions.TimeoutError:
82
+ print('Connection timed out', file=sys.stderr)
83
+ raise
84
+ except exceptions.ResponseError as e:
85
+ print(f'Response error: {e}', file=sys.stderr)
86
+ raise
87
+ except exceptions.ClusterError as e:
88
+ print(f'Valkey Cluster error: {e}', file=sys.stderr)
89
+ raise
90
+ except exceptions.ValkeyError as e:
91
+ print(f'Valkey error: {e}', file=sys.stderr)
92
+ raise
93
+ except Exception as e:
94
+ print(f'Unexpected error: {e}', file=sys.stderr)
95
+ raise
96
+
97
+ return cls._instance
@@ -0,0 +1,23 @@
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
+ from mcp.server.fastmcp import FastMCP
13
+
14
+
15
+ # Initialize FastMCP server
16
+ mcp = FastMCP(
17
+ 'awslabs.valkey-mcp-server',
18
+ instructions='Instructions for using this valkey MCP server. This can be used by clients to improve the LLM'
19
+ 's understanding of available tools, resources, etc. It can be thought of like a '
20
+ 'hint'
21
+ ' to the model. For example, this information MAY be added to the system prompt. Important to be clear, direct, and detailed.',
22
+ dependencies=['pydantic', 'loguru', 'valkey', 'dotenv', 'numpy'],
23
+ )
@@ -0,0 +1,84 @@
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 valkey MCP Server implementation."""
13
+
14
+ import argparse
15
+ from awslabs.valkey_mcp_server.common.server import mcp
16
+ from awslabs.valkey_mcp_server.tools import (
17
+ bitmap, # noqa: F401
18
+ hash, # noqa: F401
19
+ hyperloglog, # noqa: F401
20
+ json, # noqa: F401
21
+ list, # noqa: F401
22
+ misc, # noqa: F401
23
+ server_management, # noqa: F401
24
+ set, # noqa: F401
25
+ sorted_set, # noqa: F401
26
+ stream, # noqa: F401
27
+ string, # noqa: F401
28
+ )
29
+ from loguru import logger
30
+ from starlette.requests import Request # noqa: F401
31
+ from starlette.responses import Response
32
+
33
+
34
+ # Add a health check route directly to the MCP server
35
+ @mcp.custom_route('/health', methods=['GET'])
36
+ async def health_check(request):
37
+ """Simple health check endpoint for ALB Target Group.
38
+
39
+ Always returns 200 OK to indicate the service is running.
40
+ """
41
+ return Response(content='healthy', status_code=200, media_type='text/plain')
42
+
43
+
44
+ class ValkeyMCPServer:
45
+ """Valkey MCP Server wrapper."""
46
+
47
+ def __init__(self, sse=False, port=8888):
48
+ """Initialize MCP Server wrapper.
49
+
50
+ Args:
51
+ sse: Whether to use SSE transport
52
+ port: Port to run the server on (default: 8888)
53
+ """
54
+ self.sse = sse
55
+ self.port = port
56
+
57
+ def run(self):
58
+ """Run server with appropriate transport."""
59
+ if self.sse:
60
+ if self.port is not None:
61
+ mcp.settings.port = self.port
62
+ mcp.run(transport='sse')
63
+ else:
64
+ mcp.run()
65
+
66
+
67
+ def main():
68
+ """Run the MCP server with CLI argument support."""
69
+ parser = argparse.ArgumentParser(
70
+ description='An AWS Labs Model Context Protocol (MCP) server for valkey'
71
+ )
72
+ parser.add_argument('--sse', action='store_true', help='Use SSE transport')
73
+ parser.add_argument('--port', type=int, default=8888, help='Port to run the server on')
74
+
75
+ args = parser.parse_args()
76
+
77
+ logger.info('Amazon ElastiCache/MemoryDB Valkey MCP Server Started...')
78
+
79
+ server = ValkeyMCPServer(args.sse, args.port)
80
+ server.run()
81
+
82
+
83
+ if __name__ == '__main__':
84
+ main()
@@ -0,0 +1,28 @@
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
+ Tool imports for Valkey MCP Server.
14
+ """
15
+
16
+ from . import (
17
+ bitmap,
18
+ hash,
19
+ hyperloglog,
20
+ json,
21
+ list,
22
+ misc,
23
+ server_management,
24
+ set,
25
+ sorted_set,
26
+ stream,
27
+ string,
28
+ )
@@ -0,0 +1,148 @@
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
+ """Bitmap operations for Valkey MCP Server."""
13
+
14
+ from awslabs.valkey_mcp_server.common.connection import ValkeyConnectionManager
15
+ from awslabs.valkey_mcp_server.common.server import mcp
16
+ from typing import Optional
17
+ from valkey.exceptions import ValkeyError
18
+
19
+
20
+ @mcp.tool()
21
+ async def bitmap_set(key: str, offset: int, value: int) -> str:
22
+ """Set the bit at offset to value.
23
+
24
+ Args:
25
+ key: The name of the bitmap key
26
+ offset: The bit offset (0-based)
27
+ value: The bit value (0 or 1)
28
+
29
+ Returns:
30
+ Success message or error message
31
+ """
32
+ try:
33
+ if value not in (0, 1):
34
+ return f'Error: value must be 0 or 1, got {value}'
35
+ if offset < 0:
36
+ return f'Error: offset must be non-negative, got {offset}'
37
+
38
+ r = ValkeyConnectionManager.get_connection()
39
+ previous = r.setbit(key, offset, value)
40
+ return f'Bit at offset {offset} set to {value} (previous value: {previous})'
41
+ except ValkeyError as e:
42
+ return f"Error setting bit in '{key}': {str(e)}"
43
+
44
+
45
+ @mcp.tool()
46
+ async def bitmap_get(key: str, offset: int) -> str:
47
+ """Get the bit value at offset.
48
+
49
+ Args:
50
+ key: The name of the bitmap key
51
+ offset: The bit offset (0-based)
52
+
53
+ Returns:
54
+ Bit value or error message
55
+ """
56
+ try:
57
+ if offset < 0:
58
+ return f'Error: offset must be non-negative, got {offset}'
59
+
60
+ r = ValkeyConnectionManager.get_connection()
61
+ value = r.getbit(key, offset)
62
+ return f'Bit at offset {offset} is {value}'
63
+ except ValkeyError as e:
64
+ return f"Error getting bit from '{key}': {str(e)}"
65
+
66
+
67
+ @mcp.tool()
68
+ async def bitmap_count(key: str, start: Optional[int] = None, end: Optional[int] = None) -> str:
69
+ """Count the number of set bits (1) in a range.
70
+
71
+ Args:
72
+ key: The name of the bitmap key
73
+ start: Start offset (inclusive, optional)
74
+ end: End offset (inclusive, optional)
75
+
76
+ Returns:
77
+ Count of set bits or error message
78
+ """
79
+ try:
80
+ r = ValkeyConnectionManager.get_connection()
81
+ if start is not None and end is not None:
82
+ if start < 0 or end < 0:
83
+ return 'Error: start and end must be non-negative'
84
+ if start > end:
85
+ return 'Error: start must be less than or equal to end'
86
+ count = r.bitcount(key, start, end)
87
+ range_str = f' in range [{start}, {end}]'
88
+ else:
89
+ count = r.bitcount(key)
90
+ range_str = ''
91
+
92
+ return f'Number of set bits{range_str}: {count}'
93
+ except ValkeyError as e:
94
+ return f"Error counting bits in '{key}': {str(e)}"
95
+
96
+
97
+ @mcp.tool()
98
+ async def bitmap_pos(
99
+ key: str,
100
+ bit: int,
101
+ start: Optional[int] = None,
102
+ end: Optional[int] = None,
103
+ count: Optional[int] = None,
104
+ ) -> str:
105
+ """Find positions of bits set to a specific value.
106
+
107
+ Args:
108
+ key: The name of the bitmap key
109
+ bit: Bit value to search for (0 or 1)
110
+ start: Start offset (inclusive, optional)
111
+ end: End offset (inclusive, optional)
112
+ count: Maximum number of positions to return (optional)
113
+
114
+ Returns:
115
+ List of positions or error message
116
+ """
117
+ try:
118
+ if bit not in (0, 1):
119
+ return f'Error: bit must be 0 or 1, got {bit}'
120
+
121
+ r = ValkeyConnectionManager.get_connection()
122
+ args = []
123
+ if start is not None:
124
+ if start < 0:
125
+ return 'Error: start must be non-negative'
126
+ args.extend(['START', start])
127
+ if end is not None:
128
+ if end < 0:
129
+ return 'Error: end must be non-negative'
130
+ if start is not None and start > end:
131
+ return 'Error: start must be less than or equal to end'
132
+ args.extend(['END', end])
133
+ if count is not None:
134
+ if count < 1:
135
+ return 'Error: count must be positive'
136
+ args.extend(['COUNT', count])
137
+
138
+ pos = r.bitpos(key, bit, *args) if args else r.bitpos(key, bit)
139
+
140
+ if pos == -1 or pos is None:
141
+ range_str = ''
142
+ if start is not None or end is not None:
143
+ range_str = f' in range [{start or 0}, {end or "∞"}]'
144
+ return f'No bits set to {bit} found{range_str}'
145
+
146
+ return f'First bit set to {bit} found at position: {pos}'
147
+ except ValkeyError as e:
148
+ return f"Error finding bit position in '{key}': {str(e)}"