@@ -94,4 +94,93 @@ async def rename(old_key: str, new_key: str) -> Dict[str, Any]:
94
94
}
95
95
96
96
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