@@ -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