Bug: PostgresConnector never rolls back after a failed query, poisoning the shared connection for the rest of the process
Summary
wren/connector/postgres.py opens a single, long-lived psycopg connection per PostgresConnector instance and never calls .rollback() after a failed query. Since WrenEngine._get_connector() caches this connector for the engine's lifetime, and ServeContext.engine in the MCP server is a single instance shared across the whole server process, one failing query from any client leaves the shared Postgres connection in an aborted-transaction state for every subsequent query from every client, until the process is restarted.
Version
wrenai[postgres]==0.13.2 (latest on PyPI at time of filing)
Where
wren/connector/postgres.py — PostgresConnector.__init__ creates one psycopg.connect(...) and stores it as self.connection. query() and dry_run() catch exceptions and re-raise as WrenError, but never call self.connection.rollback().
wren/engine.py — _get_connector() caches the connector: if self._connector is None: self._connector = get_connector(...), reused for the lifetime of the WrenEngine instance.
wren/mcp_server.py — ServeContext is documented in-code as "Shared state captured once at startup and used by every tool handler", i.e. one Engine/connector for the whole MCP server process.
Steps to reproduce
- Start
wren serve mcp against a Postgres data source.
- Call
run_sql (or dry_run) with SQL that fails at execution time on Postgres itself (not at Wren's SQL-planning stage), e.g. a query referencing a column that doesn't exist:
SELECT nonexistent_column FROM some_table
This returns the expected error.
- Immediately call
run_sql again with a completely valid query, e.g.:
SELECT COUNT(*) FROM some_table
Expected
Step 3 succeeds normally.
Actual
Step 3 fails with:
[GENERIC_USER_ERROR] current transaction is aborted, commands ignored until end of transaction block
Every subsequent query on that server process fails the same way — from any client, any session — until the container/process is restarted.
Why this matters
Because the connector/engine is shared per-process (not per-request, per-session, or pooled), this isn't a caller-side mistake to recover from — a single bad query from one MCP client degrades the server for every other concurrent client.
Suggested fix
Roll back on failure before re-raising, e.g. in PostgresConnector.query() / dry_run():
except Exception as e:
try:
self.connection.rollback()
except Exception:
pass
raise WrenError(...) from e
A more robust fix would move to short-lived connections or an actual connection pool (e.g. psycopg_pool) with reset-on-return semantics, so a single connector instance doesn't hold one Postgres session's transaction state indefinitely.
Environment
- Reproduced via
wren serve mcp --transport http, connecting with an MCP client (mcp Python SDK, streamablehttp_client / ClientSession).
- Postgres backend (local dev instance).
Bug: PostgresConnector never rolls back after a failed query, poisoning the shared connection for the rest of the process
Summary
wren/connector/postgres.pyopens a single, long-livedpsycopgconnection perPostgresConnectorinstance and never calls.rollback()after a failed query. SinceWrenEngine._get_connector()caches this connector for the engine's lifetime, andServeContext.enginein the MCP server is a single instance shared across the whole server process, one failing query from any client leaves the shared Postgres connection in an aborted-transaction state for every subsequent query from every client, until the process is restarted.Version
wrenai[postgres]==0.13.2(latest on PyPI at time of filing)Where
wren/connector/postgres.py—PostgresConnector.__init__creates onepsycopg.connect(...)and stores it asself.connection.query()anddry_run()catch exceptions and re-raise asWrenError, but never callself.connection.rollback().wren/engine.py—_get_connector()caches the connector:if self._connector is None: self._connector = get_connector(...), reused for the lifetime of theWrenEngineinstance.wren/mcp_server.py—ServeContextis documented in-code as "Shared state captured once at startup and used by every tool handler", i.e. oneEngine/connector for the whole MCP server process.Steps to reproduce
wren serve mcpagainst a Postgres data source.run_sql(ordry_run) with SQL that fails at execution time on Postgres itself (not at Wren's SQL-planning stage), e.g. a query referencing a column that doesn't exist:run_sqlagain with a completely valid query, e.g.:Expected
Step 3 succeeds normally.
Actual
Step 3 fails with:
Every subsequent query on that server process fails the same way — from any client, any session — until the container/process is restarted.
Why this matters
Because the connector/engine is shared per-process (not per-request, per-session, or pooled), this isn't a caller-side mistake to recover from — a single bad query from one MCP client degrades the server for every other concurrent client.
Suggested fix
Roll back on failure before re-raising, e.g. in
PostgresConnector.query()/dry_run():A more robust fix would move to short-lived connections or an actual connection pool (e.g.
psycopg_pool) with reset-on-return semantics, so a single connector instance doesn't hold one Postgres session's transaction state indefinitely.Environment
wren serve mcp --transport http, connecting with an MCP client (mcpPython SDK,streamablehttp_client/ClientSession).