Skip to content

Commit cecd636

Browse files
authored
Merge pull request #26 from SPRAGE/redis-keys
Add functions to retrieve and scan keys in Redis database
2 parents 1f51ad8 + 8e9de32 commit cecd636

File tree

1 file changed

+90
-1
lines changed

1 file changed

+90
-1
lines changed

src/tools/misc.py

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,4 +94,93 @@ async def rename(old_key: str, new_key: str) -> Dict[str, Any]:
9494
}
9595

9696
except RedisError as e:
97-
return {"error": str(e)}
97+
return {"error": str(e)}
98+
99+
100+
@mcp.tool()
101+
async def scan_keys(pattern: str = "*", count: int = 100, cursor: int = 0) -> dict:
102+
"""
103+
Scan keys in the Redis database using the SCAN command (non-blocking, production-safe).
104+
105+
⚠️ IMPORTANT: This returns PARTIAL results from one iteration. Use scan_all_keys()
106+
to get ALL matching keys, or call this function multiple times with the returned cursor
107+
until cursor becomes 0.
108+
109+
The SCAN command iterates through the keyspace in small chunks, making it safe to use
110+
on large databases without blocking other operations.
111+
112+
Args:
113+
pattern: Pattern to match keys against (default is "*" for all keys).
114+
Common patterns: "user:*", "cache:*", "*:123", etc.
115+
count: Hint for the number of keys to return per iteration (default 100).
116+
Redis may return more or fewer keys than this hint.
117+
cursor: The cursor position to start scanning from (0 to start from beginning).
118+
To continue scanning, use the cursor value returned from previous call.
119+
120+
Returns:
121+
A dictionary containing:
122+
- 'cursor': Next cursor position (0 means scan is complete)
123+
- 'keys': List of keys found in this iteration (PARTIAL RESULTS)
124+
- 'total_scanned': Number of keys returned in this batch
125+
- 'scan_complete': Boolean indicating if scan is finished
126+
Or an error message if something goes wrong.
127+
128+
Example usage:
129+
First call: scan_keys("user:*") -> returns cursor=1234, keys=[...], scan_complete=False
130+
Next call: scan_keys("user:*", cursor=1234) -> continues from where it left off
131+
Final call: returns cursor=0, scan_complete=True when done
132+
"""
133+
try:
134+
r = RedisConnectionManager.get_connection()
135+
cursor, keys = r.scan(cursor=cursor, match=pattern, count=count)
136+
137+
# Convert bytes to strings if needed
138+
decoded_keys = [key.decode('utf-8') if isinstance(key, bytes) else key for key in keys]
139+
140+
return {
141+
'cursor': cursor,
142+
'keys': decoded_keys,
143+
'total_scanned': len(decoded_keys),
144+
'scan_complete': cursor == 0
145+
}
146+
except RedisError as e:
147+
return f"Error scanning keys with pattern '{pattern}': {str(e)}"
148+
149+
150+
@mcp.tool()
151+
async def scan_all_keys(pattern: str = "*", batch_size: int = 100) -> list:
152+
"""
153+
Scan and return ALL keys matching a pattern using multiple SCAN iterations.
154+
155+
This function automatically handles the SCAN cursor iteration to collect all matching keys.
156+
It's safer than KEYS * for large databases but will still collect all results in memory.
157+
158+
⚠️ WARNING: With very large datasets (millions of keys), this may consume significant memory.
159+
For large-scale operations, consider using scan_keys() with manual iteration instead.
160+
161+
Args:
162+
pattern: Pattern to match keys against (default is "*" for all keys).
163+
batch_size: Number of keys to scan per iteration (default 100).
164+
165+
Returns:
166+
A list of all keys matching the pattern or an error message.
167+
"""
168+
try:
169+
r = RedisConnectionManager.get_connection()
170+
all_keys = []
171+
cursor = 0
172+
173+
while True:
174+
cursor, keys = r.scan(cursor=cursor, match=pattern, count=batch_size)
175+
176+
# Convert bytes to strings if needed and add to results
177+
decoded_keys = [key.decode('utf-8') if isinstance(key, bytes) else key for key in keys]
178+
all_keys.extend(decoded_keys)
179+
180+
# Break when scan is complete (cursor returns to 0)
181+
if cursor == 0:
182+
break
183+
184+
return all_keys
185+
except RedisError as e:
186+
return f"Error scanning all keys with pattern '{pattern}': {str(e)}"

0 commit comments

Comments
 (0)