Skip to content

Commit b13fff0

Browse files
committed
Adding more MCP tools for data structures
1 parent 41c12d1 commit b13fff0

File tree

13 files changed

+541
-81
lines changed

13 files changed

+541
-81
lines changed

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", "pydantic"]
6+
dependencies=["redis", "dotenv"]
77
)
88

src/main.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,15 @@
33
import tools.server_management
44
import tools.misc
55
import tools.redis_query_engine
6-
import tools.hash
6+
import tools.hash
7+
import tools.list
8+
import tools.string
9+
import tools.json
10+
import tools.sorted_set
11+
import tools.set
12+
import tools.stream
13+
import tools.pub_sub
14+
715

816
class RedisMCPServer:
917
def __init__(self):

src/tools/hash.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,29 @@
22
from redis.exceptions import RedisError
33
from common.server import mcp
44

5+
56
@mcp.tool()
6-
async def hset(name: str, key: str, value: str) -> str:
7-
"""Set a field in a hash stored at key.
8-
7+
async def hset(name: str, key: str, value: str, expire_seconds: int = None) -> str:
8+
"""Set a field in a hash stored at key with an optional expiration time.
9+
910
Args:
1011
name: The Redis hash key.
1112
key: The field name inside the hash.
1213
value: The value to set.
13-
14+
expire_seconds: Optional; time in seconds after which the key should expire.
15+
1416
Returns:
1517
A success message or an error message.
1618
"""
1719
try:
1820
r = RedisConnectionManager.get_connection()
1921
r.hset(name, key, value)
20-
return f"Field '{key}' set successfully in hash '{name}'."
22+
23+
if expire_seconds is not None:
24+
r.expire(name, expire_seconds)
25+
26+
return f"Field '{key}' set successfully in hash '{name}'." + (
27+
f" Expires in {expire_seconds} seconds." if expire_seconds else "")
2128
except RedisError as e:
2229
return f"Error setting field '{key}' in hash '{name}': {str(e)}"
2330

src/tools/json.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
from common.connection import RedisConnectionManager
2+
from redis.exceptions import RedisError
3+
from common.server import mcp
4+
5+
6+
@mcp.tool()
7+
async def json_set(name: str, path: str, value: str, expire_seconds: int = None) -> str:
8+
"""Set a JSON value in Redis at a given path with an optional expiration time.
9+
10+
Args:
11+
name: The Redis key where the JSON document is stored.
12+
path: The JSON path where the value should be set.
13+
value: The JSON value to store.
14+
expire_seconds: Optional; time in seconds after which the key should expire.
15+
16+
Returns:
17+
A success message or an error message.
18+
"""
19+
try:
20+
r = RedisConnectionManager.get_connection()
21+
r.json().set(name, path, value)
22+
23+
if expire_seconds is not None:
24+
r.expire(name, expire_seconds)
25+
26+
return f"JSON value set at path '{path}' in '{name}'." + (
27+
f" Expires in {expire_seconds} seconds." if expire_seconds else "")
28+
except RedisError as e:
29+
return f"Error setting JSON value at path '{path}' in '{name}': {str(e)}"
30+
31+
32+
@mcp.tool()
33+
async def json_get(name: str, path: str = '$') -> str:
34+
"""Retrieve a JSON value from Redis at a given path.
35+
36+
Args:
37+
name: The Redis key where the JSON document is stored.
38+
path: The JSON path to retrieve (default: root '$').
39+
40+
Returns:
41+
The retrieved JSON value or an error message.
42+
"""
43+
try:
44+
r = RedisConnectionManager.get_connection()
45+
value = r.json().get(name, path)
46+
return value if value is not None else f"No data found at path '{path}' in '{name}'."
47+
except RedisError as e:
48+
return f"Error retrieving JSON value at path '{path}' in '{name}': {str(e)}"
49+
50+
51+
@mcp.tool()
52+
async def json_del(name: str, path: str = '$') -> str:
53+
"""Delete a JSON value from Redis at a given path.
54+
55+
Args:
56+
name: The Redis key where the JSON document is stored.
57+
path: The JSON path to delete (default: root '$').
58+
59+
Returns:
60+
A success message or an error message.
61+
"""
62+
try:
63+
r = RedisConnectionManager.get_connection()
64+
deleted = r.json().delete(name, path)
65+
return f"Deleted JSON value at path '{path}' in '{name}'." if deleted else f"No JSON value found at path '{path}' in '{name}'."
66+
except RedisError as e:
67+
return f"Error deleting JSON value at path '{path}' in '{name}': {str(e)}"
68+
69+
70+
@mcp.tool()
71+
async def json_expire(name: str, expire_seconds: int) -> str:
72+
"""Set an expiration time for a Redis JSON document.
73+
74+
Args:
75+
name: The Redis key where the JSON document is stored.
76+
expire_seconds: Time in seconds after which the key should expire.
77+
78+
Returns:
79+
A success message or an error message.
80+
"""
81+
try:
82+
r = RedisConnectionManager.get_connection()
83+
success = r.expire(name, expire_seconds)
84+
return f"Expiration set to {expire_seconds} seconds for JSON document '{name}'." if success else f"JSON document '{name}' does not exist."
85+
except RedisError as e:
86+
return f"Error setting expiration for JSON document '{name}': {str(e)}"

src/tools/list.py

Lines changed: 12 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -3,51 +3,32 @@
33
from common.server import mcp
44

55
@mcp.tool()
6-
async def lpush(name: str, value: str) -> str:
7-
"""Push a value onto the left of a Redis list.
8-
9-
Args:
10-
name: The Redis list key.
11-
value: The value to push.
12-
13-
Returns:
14-
A success message or an error message.
15-
"""
6+
async def lpush(name: str, value: str, expire: int = None) -> str:
7+
"""Push a value onto the left of a Redis list and optionally set an expiration time."""
168
try:
179
r = RedisConnectionManager.get_connection()
1810
r.lpush(name, value)
11+
if expire:
12+
r.expire(name, expire)
1913
return f"Value '{value}' pushed to the left of list '{name}'."
2014
except RedisError as e:
2115
return f"Error pushing value to list '{name}': {str(e)}"
2216

2317
@mcp.tool()
24-
async def rpush(name: str, value: str) -> str:
25-
"""Push a value onto the right of a Redis list.
26-
27-
Args:
28-
name: The Redis list key.
29-
value: The value to push.
30-
31-
Returns:
32-
A success message or an error message.
33-
"""
18+
async def rpush(name: str, value: str, expire: int = None) -> str:
19+
"""Push a value onto the right of a Redis list and optionally set an expiration time."""
3420
try:
3521
r = RedisConnectionManager.get_connection()
3622
r.rpush(name, value)
23+
if expire:
24+
r.expire(name, expire)
3725
return f"Value '{value}' pushed to the right of list '{name}'."
3826
except RedisError as e:
3927
return f"Error pushing value to list '{name}': {str(e)}"
4028

4129
@mcp.tool()
4230
async def lpop(name: str) -> str:
43-
"""Remove and return the first element from a Redis list.
44-
45-
Args:
46-
name: The Redis list key.
47-
48-
Returns:
49-
The removed element or an error message.
50-
"""
31+
"""Remove and return the first element from a Redis list."""
5132
try:
5233
r = RedisConnectionManager.get_connection()
5334
value = r.lpop(name)
@@ -57,14 +38,7 @@ async def lpop(name: str) -> str:
5738

5839
@mcp.tool()
5940
async def rpop(name: str) -> str:
60-
"""Remove and return the last element from a Redis list.
61-
62-
Args:
63-
name: The Redis list key.
64-
65-
Returns:
66-
The removed element or an error message.
67-
"""
41+
"""Remove and return the last element from a Redis list."""
6842
try:
6943
r = RedisConnectionManager.get_connection()
7044
value = r.rpop(name)
@@ -74,16 +48,7 @@ async def rpop(name: str) -> str:
7448

7549
@mcp.tool()
7650
async def lrange(name: str, start: int, stop: int) -> list:
77-
"""Get elements from a Redis list within a specific range.
78-
79-
Args:
80-
name: The Redis list key.
81-
start: The starting index.
82-
stop: The stopping index.
83-
84-
Returns:
85-
A list of elements or an error message.
86-
"""
51+
"""Get elements from a Redis list within a specific range."""
8752
try:
8853
r = RedisConnectionManager.get_connection()
8954
values = r.lrange(name, start, stop)
@@ -93,14 +58,7 @@ async def lrange(name: str, start: int, stop: int) -> list:
9358

9459
@mcp.tool()
9560
async def llen(name: str) -> int:
96-
"""Get the length of a Redis list.
97-
98-
Args:
99-
name: The Redis list key.
100-
101-
Returns:
102-
The length of the list or an error message.
103-
"""
61+
"""Get the length of a Redis list."""
10462
try:
10563
r = RedisConnectionManager.get_connection()
10664
return r.llen(name)

src/tools/pub_sub.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
from common.connection import RedisConnectionManager
2+
from redis.exceptions import RedisError
3+
from common.server import mcp
4+
5+
6+
@mcp.tool()
7+
async def publish(channel: str, message: str) -> str:
8+
"""Publish a message to a Redis channel.
9+
10+
Args:
11+
channel: The Redis channel to publish to.
12+
message: The message to send.
13+
14+
Returns:
15+
A success message or an error message.
16+
"""
17+
try:
18+
r = RedisConnectionManager.get_connection()
19+
r.publish(channel, message)
20+
return f"Message published to channel '{channel}'."
21+
except RedisError as e:
22+
return f"Error publishing message to channel '{channel}': {str(e)}"
23+
24+
25+
@mcp.tool()
26+
async def subscribe(channel: str) -> str:
27+
"""Subscribe to a Redis channel.
28+
29+
Args:
30+
channel: The Redis channel to subscribe to.
31+
32+
Returns:
33+
A success message or an error message.
34+
"""
35+
try:
36+
r = RedisConnectionManager.get_connection()
37+
pubsub = r.pubsub()
38+
pubsub.subscribe(channel)
39+
return f"Subscribed to channel '{channel}'."
40+
except RedisError as e:
41+
return f"Error subscribing to channel '{channel}': {str(e)}"
42+
43+
44+
@mcp.tool()
45+
async def unsubscribe(channel: str) -> str:
46+
"""Unsubscribe from a Redis channel.
47+
48+
Args:
49+
channel: The Redis channel to unsubscribe from.
50+
51+
Returns:
52+
A success message or an error message.
53+
"""
54+
try:
55+
r = RedisConnectionManager.get_connection()
56+
pubsub = r.pubsub()
57+
pubsub.unsubscribe(channel)
58+
return f"Unsubscribed from channel '{channel}'."
59+
except RedisError as e:
60+
return f"Error unsubscribing from channel '{channel}': {str(e)}"

src/tools/redis_query_engine.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from common.connection import RedisConnectionManager
33
from redis.exceptions import RedisError
44
from common.server import mcp
5+
from redis.commands.search.query import Query
56

67

78
@mcp.tool()
@@ -12,4 +13,56 @@ async def get_indexes() -> str:
1213
r = RedisConnectionManager.get_connection()
1314
return r.execute_command("FT._LIST")
1415
except RedisError as e:
15-
return f"Error retrieving indexes: {str(e)}"
16+
return f"Error retrieving indexes: {str(e)}"
17+
18+
19+
@mcp.tool()
20+
async def get_index_info(index_name: str) -> str:
21+
"""Retrieve schema and information about a specific Redis index using FT.INFO.
22+
23+
Args:
24+
index_name (str): The name of the index to retrieve information about.
25+
26+
Returns:
27+
str: Information about the specified index or an error message.
28+
"""
29+
try:
30+
r = RedisConnectionManager.get_connection()
31+
return r.ft(index_name).info()
32+
except RedisError as e:
33+
return f"Error retrieving index info: {str(e)}"
34+
35+
36+
@mcp.tool()
37+
async def get_indexed_keys(index_name: str) -> str:
38+
"""Retrieve the number of indexed keys by the index
39+
40+
Args:
41+
index_name (str): The name of the index to retrieve information about.
42+
43+
Returns:
44+
int: Number of indexed keys
45+
"""
46+
try:
47+
r = RedisConnectionManager.get_connection()
48+
return r.ft(index_name).search(Query("*")).total
49+
except RedisError as e:
50+
return f"Error retrieving number of keys: {str(e)}"
51+
52+
53+
@mcp.tool()
54+
async def execute_redis_search(command: str, *args) -> str:
55+
"""Execute a raw Redis search command like FT.SEARCH or FT.AGGREGATE.
56+
57+
Args:
58+
command (str): The Redis command to execute.
59+
*args: Additional arguments for the command.
60+
61+
Returns:
62+
str: The result of the command execution or an error message.
63+
"""
64+
try:
65+
r = RedisConnectionManager.get_connection()
66+
return r.execute_command(command, *args)
67+
except RedisError as e:
68+
return f"Error executing command {command}: {str(e)}"

0 commit comments

Comments
 (0)