Conversation
Establish the connection pool on first tool use instead of at server startup, and let idle connections be reaped, so defining many database MCP configs no longer means every server holds an open Postgres connection from session start. - server.py: store the database URL without connecting eagerly; the lazy path in SqlDriver.execute_query opens the pool on first use. - sql_driver.py: pool now uses min_size=0 with a max_idle reaper so unused/idle connections release. - Add --max-idle flag and DATABASE_MAX_IDLE env var (default 300s) to tune the idle timeout; invalid values fall back to the default via a validating max_idle property. - README: document lazy connections and the idle timeout with examples. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Would really love to see this feature implemented. Getting database timeouts after about 15 minutes of idle time |
jssmith
left a comment
There was a problem hiding this comment.
Review: Lazy database connections with configurable idle timeout
Verdict: Approve with minor comments.
Assessment
Good practical improvement — deferring pool creation to first tool use eliminates unnecessary connections when users configure many database servers. The max_idle reaper with min_size=0 means an idle server holds zero connections.
Strengths
- Lazy connect is well-implemented. Removing the eager
pool_connectfrommain()and relying on the existing lazy path inSqlDriver.execute_queryis the minimal change. max_idlevalidation is thorough. The property setter rejectsNone, zero, and negative values with a warning + fallback to default. Covers CLI flag, env var, and constructor.- README documentation includes both flag and env var examples with clear explanation of the tradeoff.
Comments
-
Removed
obfuscate_passwordimport fromserver.py. This import was used in the deleted error-logging block for the eager connect. Verify no other code paths inserver.pyreferenceobfuscate_password— if not, the removal is clean. -
db_connection.connection_url = database_urlis set directly without going through the constructor. This works becauseconnection_urlis a plain attribute, but it means the URL is stored on the pool object before any validation happens. Acceptable since validation occurs on firstpool_connect. -
The
max_idlereaper +min_size=0interaction with PR #195'sasyncio.Lockshould be considered if both PRs merge. PR #195 adds a_connect_locktopool_connect; this PR changesmin_sizeand addsmax_idle. They touch the same code and will need a merge resolution, but no logical conflict. -
No test for the lazy connect path itself — the PR description says "verified manually". A unit test confirming that
main()does not callpool_connectwould be valuable.
This review was created by an AI agent (OpenHands) on behalf of @jssmith.
jssmith
left a comment
There was a problem hiding this comment.
Review — concise
Verdict: Approve with comments. Good feature with thoughtful defaults and validation. Minor concerns around error visibility and min_size=0 behavior.
Strengths
- Lazy connection:
db_connection.connection_url = database_urlinstead ofpool_connect()at startup. Server starts without holding a Postgres connection. max_idleproperty with validation — rejectsNone, zero, and negative values, falls back toDEFAULT_MAX_IDLE(300s).--max-idleCLI flag takes precedence overDATABASE_MAX_IDLEenv var. Clear precedence chain.min_size=0onAsyncConnectionPoolmeans idle pool holds zero connections. Combined with lazy connect, a configured-but-unused server keeps zero Postgres connections.- Non-numeric env var value logs a warning and falls back to default rather than crashing.
- README documentation with both CLI flag and env var examples.
Issues to address
1. Connection errors now silent until first tool call (non-blocking, by design)
The old code called pool_connect() at startup and logged connection errors with obfuscate_password. Now, errors only surface on the first tool call. This is intentional (lazy connect), but users who rely on startup logs to verify connectivity won't see errors. The logger.info("Database URL configured; connection will be established on first use (lazy)") message helps, but consider adding a note in the startup log that connection errors will appear on first use.
2. obfuscate_password import removed but still used elsewhere (non-blocking)
The diff removes from .sql import obfuscate_password from server.py. Verify no remaining references in the file. The old startup error handling used it; if no other code path in server.py uses it, the removal is correct.
3. min_size=0 may cause connection latency on first query (non-blocking)
With min_size=0, the first query must establish a new connection. For frequently-used databases, this adds latency on the first call after idle reaping. The README mentions this trade-off ("higher values keep connections warm"). Acceptable, but worth noting for production deployments.
4. No test for max_idle validation (non-blocking)
The property setter logic (rejecting invalid values) isn't tested. A simple unit test verifying that None, 0, -1 fall back to DEFAULT_MAX_IDLE would be valuable.
5. max_idle set before connection_url is validated (non-blocking)
db_connection.max_idle is set in the argument parsing section, before the database URL is checked. If the URL is missing, the max_idle setting is applied to a pool that will never connect. Harmless, but the ordering is slightly confusing.
This review was created by an AI agent (OpenHands) on behalf of @jssmith.
Summary
Establish the connection pool on first tool use instead of at server startup, and let idle connections be reaped. This makes it practical to define many database MCP configs without every server holding an open Postgres connection from session start.
Motivation
When a user defines a large number of
postgres-mcpservers (one per database), the previous behavior opened and held a live Postgres connection for every configured server as soon as the MCP client started — even for databases that are never queried in a session. That hogs connection slots on the database side.Changes
server.py: store the database URL without connecting eagerly. The lazy path already present inSqlDriver.execute_queryopens the pool on first use, so the startuppool_connectcall was redundant.sql/sql_driver.py: the pool now usesmin_size=0with amax_idlereaper, so an unused/idle server releases its connection(s) back to zero.--max-idleflag andDATABASE_MAX_IDLEenv var (default 300s). Invalid values (non-numeric, zero, negative) fall back to the default via a validatingmax_idleproperty — a single chokepoint covering the constructor, CLI flag, and env var.Behavior
max_idleis reaped and transparently re-established on the next query (the idle timer restarts per connection).Testing
All existing unit tests pass (
tests/unit/test_transport.py,test_access_mode.py,tests/unit/sql/), and themax_idlevalidation/fallback paths were verified manually.🤖 Generated with Claude Code