Skip to content

Commit ba6bc05

Browse files
committed
draft to store vectors in hashes
1 parent fb71de9 commit ba6bc05

File tree

10 files changed

+139
-28
lines changed

10 files changed

+139
-28
lines changed

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,15 @@ To configure the Redis Cloud API MCP Server, consider the following environment
2929

3030
| Name | Description | Default Value |
3131
|-------------------------|-----------------------------------------------------------|---------------|
32-
| `REDIS_HOST` | Redis IP or hostname | '127.0.0.1' |
33-
| `REDIS_PORT` | Redis port | 6379 |
34-
| `REDIS_USERNAME` | Default database username | 'default' |
35-
| `REDIS_PWD` | Default database password | '' |
36-
| `REDIS_SSL` | Enables or disables SSL/TLS | False |
32+
| `REDIS_HOST` | Redis IP or hostname | `"127.0.0.1"` |
33+
| `REDIS_PORT` | Redis port | `6379` |
34+
| `REDIS_USERNAME` | Default database username | `"default"` |
35+
| `REDIS_PWD` | Default database password | "" |
36+
| `REDIS_SSL` | Enables or disables SSL/TLS | `False` |
3737
| `REDIS_CA_PATH` | CA certificate for verifying server | None |
3838
| `REDIS_SSL_KEYFILE` | Client's private key file for client authentication | None |
3939
| `REDIS_SSL_CERTFILE` | Client's certificate file for client authentication | None |
40-
| `REDIS_CERT_REQS` | Whether the client should verify the server's certificate | 'required' |
40+
| `REDIS_CERT_REQS` | Whether the client should verify the server's certificate | `"required"` |
4141
| `REDIS_CA_CERTS` | Path to the trusted CA certificates file | None |
4242

4343
## Integration with Claude Desktop

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ dependencies = [
88
"mcp[cli]>=1.5.0",
99
"redis>=5.2.1",
1010
"dotenv>=0.9.9",
11+
"numpy>=2.2.4",
1112
]

src/common/config.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import urllib
12
from dotenv import load_dotenv
23
import os
34

@@ -12,4 +13,44 @@
1213
"ssl_keyfile": os.getenv('REDIS_SSL_KEYFILE', None),
1314
"ssl_certfile": os.getenv('REDIS_SSL_CERTFILE', None),
1415
"ssl_cert_reqs": os.getenv('REDIS_SSL_CERT_REQS', 'required'),
15-
"ssl_ca_certs": os.getenv('REDIS_SSL_CA_CERTS', None)}
16+
"ssl_ca_certs": os.getenv('REDIS_SSL_CA_CERTS', None)}
17+
18+
19+
def generate_redis_uri():
20+
cfg = REDIS_CFG
21+
scheme = "rediss" if cfg.get("ssl") else "redis"
22+
host = cfg.get("host", "127.0.0.1")
23+
port = cfg.get("port", 6379)
24+
25+
username = cfg.get("username")
26+
password = cfg.get("password")
27+
28+
# Auth part
29+
if username:
30+
auth_part = f"{urllib.parse.quote(username)}:{urllib.parse.quote(password)}@"
31+
elif password:
32+
auth_part = f":{urllib.parse.quote(password)}@"
33+
else:
34+
auth_part = ""
35+
36+
# Base URI
37+
base_uri = f"{scheme}://{auth_part}{host}:{port}"
38+
39+
# Additional SSL query parameters if SSL is enabled
40+
query_params = {}
41+
if cfg.get("ssl"):
42+
if cfg.get("ssl_cert_reqs"):
43+
query_params["ssl_cert_reqs"] = cfg["ssl_cert_reqs"]
44+
if cfg.get("ssl_ca_certs"):
45+
query_params["ssl_ca_certs"] = cfg["ssl_ca_certs"]
46+
if cfg.get("ssl_keyfile"):
47+
query_params["ssl_keyfile"] = cfg["ssl_keyfile"]
48+
if cfg.get("ssl_certfile"):
49+
query_params["ssl_certfile"] = cfg["ssl_certfile"]
50+
if cfg.get("ssl_ca_path"):
51+
query_params["ssl_ca_path"] = cfg["ssl_ca_path"]
52+
53+
if query_params:
54+
base_uri += "?" + urllib.parse.urlencode(query_params)
55+
56+
return base_uri

src/common/connection.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
from typing import Optional
66
from common.config import REDIS_CFG
77

8+
from common.config import generate_redis_uri
9+
10+
811
class RedisConnectionManager:
912
_instance: Optional[Redis] = None
1013

src/common/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@
33
# Initialize FastMCP server
44
mcp = FastMCP(
55
"Redis MCP Server",
6-
dependencies=["redis", "dotenv"]
6+
dependencies=["redis", "dotenv", "numpy"]
77
)
88

src/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
class RedisMCPServer:
1717
def __init__(self):
18-
redis_client = RedisConnectionManager.get_connection()
18+
redis_client = RedisConnectionManager.get_connection(decode_responses=False)
1919
print(redis_client.ping())
2020

2121
def run(self):

src/tools/hash.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from common.connection import RedisConnectionManager
22
from redis.exceptions import RedisError
33
from common.server import mcp
4+
import numpy as np
45

56

67
@mcp.tool()
@@ -97,3 +98,55 @@ async def hexists(name: str, key: str) -> bool:
9798
return r.hexists(name, key)
9899
except RedisError as e:
99100
return f"Error checking existence of field '{key}' in hash '{name}': {str(e)}"
101+
102+
@mcp.tool()
103+
async def set_vector_in_hash(name: str, key: str, vector: list) -> bool:
104+
"""Store a vector as a field in a Redis hash.
105+
106+
Args:
107+
name: The Redis hash key.
108+
key: The field name inside the hash.
109+
vector: The vector (list of numbers) to store in the hash.
110+
111+
Returns:
112+
True if the vector was successfully stored, False otherwise.
113+
"""
114+
try:
115+
r = RedisConnectionManager.get_connection()
116+
117+
# Convert the vector to a NumPy array, then to a binary blob using np.float32
118+
vector_array = np.array(vector, dtype=np.float32)
119+
binary_blob = vector_array.tobytes()
120+
121+
r.hset(name, key, binary_blob)
122+
return True
123+
except RedisError as e:
124+
return f"Error storing vector in hash '{name}' with key '{key}': {str(e)}"
125+
126+
127+
@mcp.tool()
128+
async def get_vector_from_hash(name: str, key: str):
129+
"""Retrieve a vector from a Redis hash and convert it back from binary blob.
130+
131+
Args:
132+
name: The Redis hash key.
133+
key: The field name inside the hash.
134+
135+
Returns:
136+
The vector as a list of floats, or an error message if retrieval fails.
137+
"""
138+
try:
139+
r = RedisConnectionManager.get_connection(decode_responses=False)
140+
141+
# Retrieve the binary blob stored in the hash
142+
binary_blob = r.hget(name, key)
143+
144+
if binary_blob:
145+
# Convert the binary blob back to a NumPy array (assuming it's stored as float32)
146+
vector_array = np.frombuffer(binary_blob, dtype=np.float32)
147+
return vector_array.tolist()
148+
else:
149+
return f"Field '{key}' not found in hash '{name}'."
150+
151+
except RedisError as e:
152+
return f"Error retrieving vector from hash '{name}' with key '{key}': {str(e)}"

src/tools/misc.py

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,6 @@
44
from common.server import mcp
55

66

7-
@mcp.tool()
8-
async def execute_raw_command(command: str, *args) -> str:
9-
"""Execute a raw Redis raw command like SET, GET, HSET, HGET, JSON.SET, JSON.GET, FT.SEARCH or FT.AGGREGATE.
10-
Every Redis command can be executed using this call
11-
12-
Args:
13-
command (str): The Redis command to execute.
14-
*args: Additional arguments for the command.
15-
16-
Returns:
17-
str: The result of the command execution or an error message.
18-
"""
19-
try:
20-
r = RedisConnectionManager.get_connection()
21-
return r.execute_command(command, *args)
22-
except RedisError as e:
23-
return f"Error executing command {command}: {str(e)}"
24-
257
@mcp.tool()
268
async def get_key_info(key: str) -> Dict[str, Any]:
279
try:

src/tools/redis_query_engine.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ async def get_index_info(index_name: str) -> str:
3434

3535

3636
@mcp.tool()
37-
async def get_indexed_keys(index_name: str) -> str:
37+
async def get_indexed_keys_number(index_name: str) -> str:
3838
"""Retrieve the number of indexed keys by the index
3939
4040
Args:
@@ -48,3 +48,4 @@ async def get_indexed_keys(index_name: str) -> str:
4848
return r.ft(index_name).search(Query("*")).total
4949
except RedisError as e:
5050
return f"Error retrieving number of keys: {str(e)}"
51+

uv.lock

Lines changed: 30 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)