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 +13 -0
- awslabs/valkey_mcp_server/__init__.py +14 -0
- awslabs/valkey_mcp_server/common/__init__.py +20 -0
- awslabs/valkey_mcp_server/common/config.py +85 -0
- awslabs/valkey_mcp_server/common/connection.py +97 -0
- awslabs/valkey_mcp_server/common/server.py +23 -0
- awslabs/valkey_mcp_server/main.py +84 -0
- awslabs/valkey_mcp_server/tools/__init__.py +28 -0
- awslabs/valkey_mcp_server/tools/bitmap.py +148 -0
- awslabs/valkey_mcp_server/tools/hash.py +283 -0
- awslabs/valkey_mcp_server/tools/hyperloglog.py +58 -0
- awslabs/valkey_mcp_server/tools/json.py +422 -0
- awslabs/valkey_mcp_server/tools/list.py +376 -0
- awslabs/valkey_mcp_server/tools/misc.py +104 -0
- awslabs/valkey_mcp_server/tools/server_management.py +54 -0
- awslabs/valkey_mcp_server/tools/set.py +182 -0
- awslabs/valkey_mcp_server/tools/sorted_set.py +359 -0
- awslabs/valkey_mcp_server/tools/stream.py +343 -0
- awslabs/valkey_mcp_server/tools/string.py +228 -0
- awslabs/valkey_mcp_server/version.py +12 -0
- awslabs_valkey_mcp_server-0.1.1.dist-info/METADATA +164 -0
- awslabs_valkey_mcp_server-0.1.1.dist-info/RECORD +26 -0
- awslabs_valkey_mcp_server-0.1.1.dist-info/WHEEL +4 -0
- awslabs_valkey_mcp_server-0.1.1.dist-info/entry_points.txt +2 -0
- awslabs_valkey_mcp_server-0.1.1.dist-info/licenses/LICENSE +175 -0
- awslabs_valkey_mcp_server-0.1.1.dist-info/licenses/NOTICE +2 -0
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)}"
|