66import re
77from threading import RLock
88import uuid
9+ import threading
910from typing import Dict , List , Any , Optional
1011import pymysql
1112import sqlite3
@@ -52,6 +53,8 @@ class ConnectionManager:
5253 def __init__ (self ):
5354 self .connections : Dict [str , Any ] = {}
5455 self .connection_configs : Dict [str , DatabaseConfig ] = {}
56+ self ._registry_lock = threading .RLock ()
57+ self ._connection_locks : Dict [str , Any ] = {}
5558 self ._metadata_cache : OrderedDict [
5659 tuple [str , str , Optional [str ]], Dict [str , Any ]
5760 ] = OrderedDict ()
@@ -152,16 +155,21 @@ def create_connection(self, config: DatabaseConfig) -> str:
152155 )
153156 connection .autocommit = True
154157 elif config .type == DatabaseType .SQLITE :
155- connection = sqlite3 .connect (config .database )
158+ # MCP database work runs in worker threads. SQLite's default
159+ # thread affinity would reject a connection created elsewhere.
160+ connection = sqlite3 .connect (config .database , check_same_thread = False )
156161 connection .row_factory = sqlite3 .Row # Enable dict-like access
157162 else :
158163 raise DatabaseConnectionError (f"Unsupported database type: { config .type } " )
159164
160- self .connections [connection_id ] = connection
161- # Store config without sensitive data for reference
162- safe_config = config .model_copy ()
163- safe_config .password = "***" # Mask password
164- self .connection_configs [connection_id ] = safe_config
165+ # Store the connection and its lock atomically with the registry.
166+ with self ._registry_lock :
167+ self .connections [connection_id ] = connection
168+ self ._connection_locks [connection_id ] = threading .RLock ()
169+ # Store config without sensitive data for reference
170+ safe_config = config .model_copy ()
171+ safe_config .password = "***" # Mask password
172+ self .connection_configs [connection_id ] = safe_config
165173
166174 logger .info (f"Created connection { connection_id } to { config .type } ://{ config .host } :{ config .port } " )
167175 return connection_id
@@ -184,23 +192,54 @@ def get_connection(self, connection_id: str) -> Any:
184192 Raises:
185193 DatabaseConnectionError: If connection not found
186194 """
187- if connection_id not in self .connections :
195+ with self ._connection_lock (connection_id ):
196+ connection = self ._get_connection_unlocked (connection_id )
197+ try :
198+ if getattr (connection , "closed" , 0 ):
199+ raise DatabaseConnectionError ("connection is closed" )
200+ if hasattr (connection , "ping" ):
201+ connection .ping (reconnect = True )
202+ except Exception as exc :
203+ logger .warning ("Connection %s is dead, removing: %s" , connection_id , exc )
204+ self ._close_connection_unlocked (connection_id , connection )
205+ raise DatabaseConnectionError (
206+ f"Connection { connection_id } is no longer valid"
207+ ) from exc
208+ return connection
209+
210+ @contextmanager
211+ def _connection_lock (self , connection_id : str ):
212+ """Serialize operations for one connection without blocking the registry."""
213+ with self ._registry_lock :
214+ lock = self ._connection_locks .get (connection_id )
215+ if lock is None or connection_id not in self .connections :
216+ raise DatabaseConnectionError (f"Connection { connection_id } not found" )
217+ with lock :
218+ yield
219+
220+ def _get_connection_unlocked (self , connection_id : str ) -> Any :
221+ with self ._registry_lock :
222+ connection = self .connections .get (connection_id )
223+ if connection is None :
188224 raise DatabaseConnectionError (f"Connection { connection_id } not found" )
189-
190- connection = self .connections [connection_id ]
191-
192- # Test connection is still alive
193- try :
194- if getattr (connection , "closed" , 0 ):
195- raise DatabaseConnectionError ("connection is closed" )
196- if hasattr (connection , 'ping' ):
197- connection .ping (reconnect = True )
198- except Exception as e :
199- logger .warning (f"Connection { connection_id } is dead, removing: { e } " )
200- self .close_connection (connection_id )
201- raise DatabaseConnectionError (f"Connection { connection_id } is no longer valid" )
202-
203225 return connection
226+
227+ def _close_connection_unlocked (self , connection_id : str , connection : Any ) -> bool :
228+ """Close and remove a connection while its per-connection lock is held."""
229+ try :
230+ connection .close ()
231+ closed = True
232+ except Exception as exc :
233+ logger .error ("Error closing connection %s: %s" , connection_id , exc )
234+ closed = True
235+ finally :
236+ with self ._registry_lock :
237+ self .connections .pop (connection_id , None )
238+ self .connection_configs .pop (connection_id , None )
239+ self ._connection_locks .pop (connection_id , None )
240+ self .invalidate_metadata_cache (connection_id )
241+ logger .info ("Closed connection %s" , connection_id )
242+ return closed
204243
205244 def close_connection (self , connection_id : str ) -> bool :
206245 """
@@ -212,25 +251,19 @@ def close_connection(self, connection_id: str) -> bool:
212251 Returns:
213252 True if connection was closed, False if not found
214253 """
215- if connection_id not in self .connections :
216- self .invalidate_metadata_cache (connection_id )
217- return False
218-
219- try :
220- connection = self .connections [connection_id ]
221- connection .close ()
222- self .connections .pop (connection_id , None )
223- self .connection_configs .pop (connection_id , None )
224- self .invalidate_metadata_cache (connection_id )
225- logger .info (f"Closed connection { connection_id } " )
226- return True
227- except Exception as e :
228- logger .error (f"Error closing connection { connection_id } : { e } " )
229- # Remove from dict anyway
230- self .connections .pop (connection_id , None )
231- self .connection_configs .pop (connection_id , None )
232- self .invalidate_metadata_cache (connection_id )
233- return True
254+ with self ._registry_lock :
255+ if connection_id not in self .connections :
256+ self .invalidate_metadata_cache (connection_id )
257+ return False
258+ lock = self ._connection_locks .setdefault (connection_id , threading .RLock ())
259+ with lock :
260+ # A preceding close may have removed the connection while this
261+ # caller was waiting for the per-connection lock.
262+ with self ._registry_lock :
263+ current = self .connections .get (connection_id )
264+ if current is None :
265+ return False
266+ return self ._close_connection_unlocked (connection_id , current )
234267
235268 def get_connection_info (self , connection_id : str ) -> Optional [DatabaseConfig ]:
236269 """
@@ -242,7 +275,8 @@ def get_connection_info(self, connection_id: str) -> Optional[DatabaseConfig]:
242275 Returns:
243276 Database configuration or None if not found
244277 """
245- config = self .connection_configs .get (connection_id )
278+ with self ._registry_lock :
279+ config = self .connection_configs .get (connection_id )
246280 return config .model_copy (deep = True ) if config else None
247281
248282 def list_connections (self ) -> Dict [str , Dict [str , Any ]]:
@@ -252,15 +286,18 @@ def list_connections(self) -> Dict[str, Dict[str, Any]]:
252286 Returns:
253287 Dict of connection_id -> connection_info
254288 """
289+ with self ._registry_lock :
290+ configs = list (self .connection_configs .items ())
291+ active_ids = set (self .connections )
255292 result = {}
256- for conn_id , config in self . connection_configs . items () :
293+ for conn_id , config in configs :
257294 result [conn_id ] = {
258295 "type" : config .type ,
259296 "host" : config .host ,
260297 "port" : config .port ,
261298 "database" : config .database ,
262299 "username" : config .username ,
263- "status" : "active" if conn_id in self . connections else "closed"
300+ "status" : "active" if conn_id in active_ids else "closed"
264301 }
265302 return result
266303
@@ -275,12 +312,33 @@ def get_cursor(self, connection_id: str):
275312 Yields:
276313 Database cursor
277314 """
278- connection = self .get_connection (connection_id )
279- cursor = connection .cursor ()
280- try :
281- yield cursor
282- finally :
283- cursor .close ()
315+ with self ._connection_lock (connection_id ):
316+ connection = self ._get_connection_unlocked (connection_id )
317+ try :
318+ if getattr (connection , "closed" , 0 ):
319+ raise DatabaseConnectionError ("connection is closed" )
320+ if hasattr (connection , "ping" ):
321+ connection .ping (reconnect = True )
322+ except Exception as exc :
323+ logger .warning ("Connection %s is dead, removing: %s" , connection_id , exc )
324+ self ._close_connection_unlocked (connection_id , connection )
325+ raise DatabaseConnectionError (
326+ f"Connection { connection_id } is no longer valid"
327+ ) from exc
328+ try :
329+ cursor = connection .cursor ()
330+ except Exception as exc :
331+ logger .warning ("Connection %s could not create a cursor: %s" , connection_id , exc )
332+ self ._close_connection_unlocked (connection_id , connection )
333+ raise DatabaseConnectionError (
334+ f"Connection { connection_id } is no longer valid"
335+ ) from exc
336+ try :
337+ # Exceptions from the caller's SQL body must propagate without
338+ # evicting a healthy connection from the registry.
339+ yield cursor
340+ finally :
341+ cursor .close ()
284342
285343 def execute_query (self , connection_id : str , query : str , params : Optional [tuple ] = None ) -> List [Dict [str , Any ]]:
286344 """
0 commit comments