Releases: Idle-Sync/db-conn-mcp
Release list
v0.7.1
A security-hardening pass on the write-safety gate and the HTTP transport, from a full-repo
audit. Read the breaking changes first — the HTTP transport now requires a token.
Security fixes
-
A dry-run can no longer permanently commit via a stacked statement. Previously,
execute_write_query(sql="DELETE FROM t; COMMIT;", dry_run=true)sent both commands over
the driver's simple-query protocol; the embeddedCOMMITended the dry-run's wrapping
transaction before it could roll back, so the change was committed for real while the tool
reportedrolled_back: true— bypassing the wholemode → dry-run → yolo → user_consent
gate. The write path now rejects multi-statement SQL before executing. -
Dry-run grants are scoped to the MCP session. A dry-run preview under one session no
longer authorizes a commit of the identical statement from a different client. Under the
httptransport a single server process serves many clients; a grant made by one is now
invisible to the others.stdio(one client per process) is unaffected. -
The
http/SSE transport now requires authentication. It previously served every tool,
includingexecute_write_query, on127.0.0.1:8000with no credential — any local process
could drive writes. Every request must now carryAuthorization: Bearer <token>; the token
is minted on first start, printed to the terminal, and persisted owner-only at
~/.db-conn-mcp/http-token. Requests without a valid token get401; requests whoseHost
header isn't loopback get403(a DNS-rebinding defense).stdiois unaffected.
Breaking / Behaviour changes
-
--transport httpclients must now send an auth token. After upgrading, an HTTP/SSE
client that worked before will get401 Unauthorizeduntil you configure it with the
headerAuthorization: Bearer <token>, where<token>is printed at server startup and
stored at~/.db-conn-mcp/http-token. Clients connecting from a non-loopbackHostare
refused with403.stdioclients (the default) need no changes. -
execute_write_querynow rejects SQL containing more than one statement. If you relied
on sending several;-separated statements in one call, split them into separate calls;
you will otherwise get a sanitizedValueError(Multiple SQL statements are not allowed; submit a single statement.). Semicolons inside string literals, dollar-quoted bodies, and
comments are fine — only actual statement boundaries are rejected.
v0.7.0
The tools get sharper: bounds on everything unbounded, loud rejection of arguments that used
to vanish silently, raw database values fenced on every channel a client can read, and a
one-line nudge when a newer release exists. Still 23 tools and 2 prompts. Read the
breaking changes first — three of them alter what existing consumers observe.
Breaking / Behaviour changes
-
The four tools that return raw row values no longer send a
structuredContent
channel.sample_table_rows,execute_read_query,fetch_rows, andsearch_value
now answer on the text channel only — the same JSON as before, inside the
<<<UNTRUSTED DATABASE DATA …>>>banner — and they no longer advertise an output
schema. If you parsedstructuredContentfrom any of these, read the text channel
instead. A result with no rows is still an explicit answer: the banner contains an
empty JSON list[], so "the table is empty" never arrives as an empty response. Why: that banner is the prompt-injection defence, and row values are the most
attacker-controllable thing this server emits; a client that renders only the
structured channel was seeing exactly that data with no fence around it. Every other
tool (schemas, table stats, diagnostics, config) keeps itsstructuredContent
unchanged. -
check_sequencesnow returns only the problem sequences by default — pass
behind_only=falsefor the full census. A database with 130 healthy sequences used
to send all 130 rows back just to report the one that was stale; the default answer is
now the stale ones alone, plus a newtotal_sequencesfield saying how many were
checked, sobehind_count: 0still reads as an affirmative "all clear" rather than an
empty result.behind_only=falserestores exactly the previous list (also with
total_sequencesadded). If you consumedsequencesas a complete inventory of every
sequence, passbehind_only=false. -
A tool call carrying a parameter the tool doesn't have is now rejected instead of
quietly ignored. Previously the unknown argument was dropped and the tool ran anyway
— callingcheck_database(connection="prod")probed every configured database while
looking like it had targeted one. Such a call now comes back as an error naming the
unknown parameter(s), listing the ones the tool accepts, and suggesting the closest
match for a near-miss spelling. Only parameter names appear in the message, never
their values. If a client of yours passes extra arguments, it will now see errors where
it previously saw (wrong) results — fix the argument name. Some clients also send stray
or dummy arguments of their own, including to tools that take no parameters at all (for
examplelist_databases); those calls now error too, and the error names the offending
argument alongside the parameters the tool accepts.
Added
-
Interactive commands now tell you when a newer release is out. After a command
likedb-conn-mcp statusordoctorfinishes, a single line points at the newer
version and how to upgrade. It only ever appears in a real terminal, is looked up in
the background so it can never slow a command down (offline just means no line), and
never changes the command's exit code. SetDB_CONN_MCP_NO_UPDATE_CHECK=1to turn it
off. The MCP server path never checks — a client launching db-conn-mcp makes no
network calls of ours. -
Three exploration tools can now be asked the narrower question directly.
table_statstakestable(one table, optionally schema-qualified),min_size_bytes
(skip anything smaller), andlimit(top N by total size);list_tablestakes
pattern(fuzzy, case-insensitive name match, the same onefind_columnsuses) and
limit;find_columnstakeslimit. All filtering happens in the database, so a huge
schema no longer has to come back in full just to answer "which is the biggest table".
When you passlimittotable_statsthe result carriestruncated: true/false;
list_tablesandfind_columnsstill return a plain list of rows, simply a shorter
one. Omit the new arguments and every one of these tools answers exactly as before. -
explain_querynow takesparams, andget_table_schemacan list a table's
indexes.explain_query(sql, params=[…])binds$1/$2values through the driver
exactly likeexecute_read_query, so you get the plan for the query you are actually
about to run instead of having to rewrite it with literals — which can plan
differently — and it composes withanalyze=true.get_table_schema(..., include_indexes=true)adds anindexeslist to the answer: each index's name, its key
columns in index order (expression indexes report their expression; an INCLUDE payload
is not a key column, so it is not listed), whether it is
unique, and its method (btree, gin, …) — so "is this column indexed?" no longer needs a
hand-written catalog query. Both are additive: withoutparamsthe plan is exactly
what it was, and withoutinclude_indexesthe schema response has noindexeskey at
all.
Changed
-
doctorno longer says "you're up to date" beside a server process that isn't.
After apipx upgrade, a still-running client process keeps serving the old build —
yet the release check happily reportedv0.5.6 is the latest published version, which
reads as an all-clear at exactly the wrong moment. When a pre-upgrade process is still
running, that line now ends— but a running process predates this install; see process_staleness. Nothing changes when no stale process is found, and whenpsutil
is missing (staleness is unknowable) the wording is untouched. -
Doctor findings that tell you what to do now say it in
suggested_actiontoo.
Failing checks used to shipsuggested_action: "none"with the actual remedy buried in
prose. Four sites now carry a machine-actionable verb: an unreachable database →
fix_connection, a check that crashed →report_bug, the process check with no
psutilinstalled →install_doctor_extra, and "no configuration found" →
run_setup. If you match on the action vocabulary, add those four values; details and
statuses are unchanged. -
The tool descriptions your agent reads now state the size and the channel up front.
Every tool whose answer grows with your database (list_tables,find_columns,
table_stats,check_sequences) says so in its description and names the argument
that bounds it, so an agent reaches forlimit/patterninstead of pulling a whole
schema back to filter it in context; and the four tools that return raw row values say
outright that their rows come back on the text channel only, inside the untrusted-data
banner. Descriptions only — no tool's behaviour changed because of this entry.
v0.6.2
The dashboard grows a front door and a face: a bare visit now tells you how in, a
bookmark keeps working, and the page looks like the instrument it is. Still 23 tools and
2 prompts — nothing about querying your databases changed.
Changed
- The dashboard has been restyled as a bench instrument. It now reads like a piece of
diagnostic equipment sitting next to your terminal rather than a web app: one column,
engraved section labels, dense bordered rows instead of floating cards, quiet outlined
buttons, and colour spent only on state. Every row carries a status lamp next to its
title — hollow when idle, pulsing while an action is in flight, and green / amber / red
once it settles — so you can see what your plumbing is doing without reading a word. The
"your host process stopped" notice is now a mains-warning strip, and the whole page
follows your system light/dark setting with contrast checked in both. Nothing you click
behaves differently; only the appearance changed. - The "you need a token" page you get from visiting
http://127.0.0.1:31415by hand now
looks like part of the tool instead of an unstyled browser default. It styles itself
from a small inline block, because the real stylesheet sits behind the same guard that
refused you — so that one response is served under a stricter policy than the rest of
the dashboard (default-src 'none'; style-src 'unsafe-inline'): it may not fetch a
script, image, font, frame or request of any kind, from anywhere. It still refuses you,
still with a 403, and still discloses nothing but the tool's name anddb-conn-mcp gui.
Added
- Opening
http://127.0.0.1:31415without a token now tells you how to get in. Instead
of a raw{"error": "forbidden"}with no way forward, a browser navigating to the
dashboard gets a small page saying it needs a token and to rundb-conn-mcp gui. The
request is still refused (it is still a 403, and it still discloses nothing else) — only
what a human sees changed. Every other unauthenticated request keeps the same opaque JSON
refusal it always had. - The dashboard URL is now bookmarkable for the life of the server run. Opening the page
with its token also sets a session cookie, so reloading the tab — or visiting the bare
http://127.0.0.1:31415after the?token=is gone — keeps working instead of dropping
you back to a 403. The cookie isHttpOnlyandSameSite=Strict, it disappears when you
close the browser, and it authorises reads only: anything that spawns a process,
editsconnections.json, or writes a client config still requires the real token. As
before, restarting the server mints a new token and retires every existing session. - An empty Databases list now explains itself instead of rendering nothing: it points
you at the Add form below, and says outright when adding one will create
connections.jsonin your home directory.
v0.6.1
One dashboard polish, found in first real-world use. No other changes.
Fixed
- The dashboard no longer leaves you staring at frozen cards. When the host process
stops (or the session expires), the notice explaining it now scrolls itself into view
instead of sitting off screen at the top of the page, and every action it interrupted is
released: stuckconnecting.../verifying...labels are replaced with
stopped - see the notice at the top of the page, and the buttons they disabled become
clickable again.
v0.6.0
A browser dashboard, and the first proof your setup actually speaks MCP. Still 23 tools and
2 prompts — nothing about querying your databases changed, but read the behaviour changes
below: the server now hosts the dashboard by default.
Added
-
A browser dashboard — the CLI's equal, clickable. One page on
http://127.0.0.1:31415, in three sections:- Databases — add, edit, remove and test your connections without hand-editing
connections.json. A stored DSN is never shown, by anything, ever: the field is
write-only, so an edit form starts blank and leaving it blank keeps the DSN already
saved. A connection's name is fixed once created (to rename, remove and re-add), and
fallback ports can be set or changed from here. A name that is blank, padded with
spaces, or contains a/is refused: the dashboard could save one, but could never
edit, test or remove it again. - Clients — the same nine MCP clients the wizard knows, each with inject/uninject
buttons and the exact command and arguments that client would launch. A client
whose config file cannot be parsed is listed and explained, never written to — the same
refusalsetupandclientsmake. - Verify & Doctor — the live verification below, plus the full
doctorsweep with the
sameok/warn/fail/skippedfindings the CLI prints.
- Databases — add, edit, remove and test your connections without hand-editing
-
Live MCP verification — "does the binary my client launches actually answer?" For each
detected client, the dashboard spawns the exact command and arguments stored in that
client's own config and holds a real MCP conversation with it using the SDK's own client
library:initialize, thentools/list(23 expected), then a reallist_databasescall.
The verdict is one ofanswers,launch_failed,handshake_failed,wrong_tool_count,
timeout— evidence, not a guess. The dashboard never answers from its own process, so a
client pointed at a different install is caught rather than masked. One more button runs
the same check over the HTTP (SSE) transport (port_in_usewhen port 8000 is already
taken). -
Stale-install detection. When the server a client launches reports a different version
than the dashboard itself is running, the result is flagged as stale with the upgrade
command — the "you upgraded but that client still starts the old copy" case, now visible
per client rather than inferred. -
db-conn-mcp guiopens the dashboard: it reuses the one a running server is already
hosting, or starts a standalone one (which shuts itself down after 15 idle minutes) and
opens your browser at it.db-conn-mcp setupnow ends with a tip pointing at the command,
so a first-time user discovers the dashboard instead of never hearing about it. -
A dashboard tab left open from before a restart says so. Each start mints a fresh
token, so an old tab's requests are refused; the page now shows a single banner asking
you to rundb-conn-mcp guiagain, instead of every panel failing for no stated reason.
Breaking / Behaviour changes
-
Starting the MCP server now also starts a local dashboard listener on
127.0.0.1:31415. Every server start hosts the dashboard alongside the MCP protocol;
the first server process to start wins the port and the rest skip it silently, so several
clients running the server at once still means exactly one dashboard. It listens on
loopback only — no connection from another machine can reach it — and every request,
including the page itself, must carry a secret token generated at start-up and stored in a
user-only file at~/.db-conn-mcp/gui-token. To turn it off, add--no-guito the
db-conn-mcp command in your client's config. If port 31415 is already taken by something
else on your machine, the server simply carries on without a dashboard. -
The server now reports its own version to your MCP client. During the initialize
handshake,serverInfo.versionused to be the version of the underlying MCP SDK (e.g.
1.27.2) because the SDK fills that in when a server does not supply one. It is now the
db-conn-mcp version (e.g.0.5.6). If you script against that field, expect our version
there from now on — and it finally lets a client tell which build of db-conn-mcp it is
talking to.
Changed
- Three dependencies are now declared explicitly:
starlette,uvicornand
httpx. All three already arrived withmcp, but the dashboard imports them
directly, and an import of ours must not rest on somebody else's transitive pin. db_conn_mcp.server.run()takes a newgui=Truekeyword (the CLI passesnot --no-gui).
Calling it as before is unchanged apart from the listener described above.
v0.5.6
Codex joins the setup targets, and no client config you have is overwritten any more when it
cannot be read. Still 23 tools and 2 prompts — nothing about querying your databases changed.
Added
-
Codex is now a setup target.
db-conn-mcp setupanddb-conn-mcp clientsdetect
~/.codex/config.toml(or$CODEX_HOME/config.toml) and write a[mcp_servers.db-conn-mcp]
entry, bringing the auto-injection list to nine clients. One entry covers the ChatGPT
desktop app, the Codex CLI and the IDE extension, which share that file. Your comments,
formatting and other MCP servers in that file are preserved. The entry carries an explicit
startup_timeout_sec = 30, because Codex's 10s default can be tight for Python startup on
a cold disk. -
doctornow flags a client config it cannot read. A detected MCP client whose config
file does not parse gets aclient_pathswarning (repair_client_config) telling you to fix
that file by hand and re-rundb-conn-mcp clients. Previouslyclientsandstatusboth
showed the problem whiledoctor— the one command whose whole job is diagnostics — stayed
silent about it. The finding names the client and the path, never the file's contents.
Fixed
- A client config that is not valid UTF-8 no longer crashes
db-conn-mcp status. Reading a
client's config used to guard against a bad-JSON or an I/O error but not against undecodable
bytes, so a config saved in a non-UTF-8 encoding took the whole command down with a traceback.
Such a file is now treated like any other unreadable config: the client is listed as
config unreadableand left untouched.doctorand the injection commands were affected the
same way.
Breaking / Behaviour changes
- A client config file that exists but does not parse is no longer overwritten. Previously,
ifsetuporclientscould not read a client's config, it treated the file as empty and
wrote a fresh one — silently discarding whatever was in there, including your other MCP
servers. It now skips that client, tells you which file it could not parse, and leaves the
file untouched. The client still appears in every listing —setup,statusand
clients --remove— markedconfig unreadable, so you can fix it by hand and re-run.
clients --removereports it without offering it as a removal target (uninjecting means
rewriting the file, which is exactly what we refuse to do), so it no longer answers a broken
config with a bare "not injected into any detected MCP client". "Could not read" covers a
syntax error, an unreadable file, and a file whose top level is not an object. This affects
all nine clients, not just Codex.
Changed
- Adds one dependency,
tomlkit(pure Python, no transitive dependencies). Codex's config
is TOML, and the standard library has a TOML reader but no writer at any Python version.
v0.5.5
Documentation and packaging only. No code change, no behaviour change — still 23 tools and
2 prompts, and nothing about how the server runs is different from 0.5.4.
Added
- This changelog, covering all releases back to 0.1.0. Entries for 0.5.2 and earlier were
backfilled from each release's own contemporaneous notes. - A Changelog link on the PyPI project page, via a
Changelogproject URL.
Changed
-
Release notes now come from this file. The release workflow extracts the tag's section and
passes it togh release create --notes-file, so GitHub, PyPI and the repo cannot drift
apart. A missing section logs a warning and falls back to generated notes, so a release can
never fail for want of prose.This replaces
--generate-notes, which emitted PR-title lists. That was how 0.5.3 shipped a
breaking change to tool output described only as "Fence tool output as untrusted database
data" — accurate about the work, silent about the consequence. -
The notes for 0.5.2, 0.5.3 and 0.5.4 have been rewritten from their changelog entries.
0.1.0 through 0.5.1 keep their original hand-written notes, which were already better than
anything a regeneration would produce. -
Rule 7 (living documentation) now covers
CHANGELOG.md, so entries are written with the
change rather than remembered afterwards.
v0.5.4
Infrastructure only. No tool was added, removed or changed — still 23 tools and 2 prompts.
Breaking / Behaviour changes
-
Python 3.10 and 3.11 are no longer supported. The floor is now 3.12. Python 3.10
reaches end of life on 2026-10-31 and 3.11 is already security-only. If you are on 3.10 or
3.11,pip install --upgrade db-conn-mcpwill keep you on 0.5.3 rather than fail — pip
honoursrequires-python— so the effect is silent: you simply stop receiving updates.
Install on 3.12+ to continue.Note this shipped as a patch bump. Under semver, dropping interpreters argues for a minor.
Added
.github/workflows/ci.yml— lint and the full test suite on every push tomainand every
pull request: Python 3.12, 3.13 and 3.14 on Linux, plus the floor version on Windows and
macOS (client_specs()branches three ways onsys.platform). Previouslypublish.ymlwas
the only workflow, it runs on tags only, and it never ranpytestorruff— so nothing had
ever been gated on the tests. The green "Analyze (python)" check on pull requests is GitHub's
default CodeQL setup, not this project's suite.- Python 3.13 and 3.14 classifiers. Both were missing, so the package did not advertise support
for either actively-maintained branch.
Changed
ruffis pinned exactly (ruff==0.16.2) in thedevextra rather than floored, and CI
installs it from that extra sopyproject.tomlstays the single source of truth. A floor let
CI resolve a newer ruff than a developer had locally and fail on rules they could not
reproduce.- Markdown is excluded from ruff. Ruff 0.16+ formats Python blocks inside
.md, and doc
snippets here are illustrative whiledocs/superpowers/holds dated artifacts that must stay
verbatim. - Internal modernisation unlocked by the new floor:
asyncio.TimeoutError→TimeoutError
(the same object since 3.11) andtimezone.utc→datetime.UTC. No behaviour change.
Full Changelog: v0.5.3...v0.5.4
v0.5.3
Breaking / Behaviour changes
-
A tool result's text content block is no longer bare JSON. Every result is now fenced
between<<<UNTRUSTED DATABASE DATA …>>>markers, sojson.loads(result.content[0].text)
will raise.structuredContentis unchanged and byte-identical, so clients that read it — the
spec-conforming path, and what all 23 tools declare an output schema for — are unaffected.
Only code parsing the text channel needs updating. Alist[dict]tool emits one text block
per item and each is fenced individually. -
A failed commit now consumes its dry-run grant. Previously the grant survived, letting
an agent retry the identical statement immediately. Because a commit can fail ambiguously —
a statement timeout, or a connection dropped duringCOMMIT, may still have applied the
write server-side — that retry could silently double-apply a non-idempotent statement such as
UPDATE t SET n = n + 1. One preview now authorises exactly one commit attempt, so a failed
commit requires a fresh dry-run that shows the agent the current state before it retries.This reverses a deliberate earlier choice that favoured retry ergonomics.
Added
-
Prompt-injection hardening (
guard.py). Database content is attacker-controllable: anyone
who can insert a row, or name a table or column, chooses text that lands verbatim in the
agent's context. Two layers, both advisory to the model:- Every tool result's text channel is fenced with an explicit "this is data, not
instructions" notice, applied at a single seam (GuardedFastMCP.call_tool) rather than in
each tool. - A standing policy travels in the server's
initializeinstructions — the durable half,
since a client reading onlystructuredContentnever sees the per-response fence.
Markers found inside a payload are defanged first, so hostile content cannot close the fence
early and appear to speak with the server's authority.This is mitigation, not a guarantee: a determined injection can still influence a model.
- Every tool result's text channel is fenced with an explicit "this is data, not
Changed
- Both raw-SQL tool descriptions now tell the agent to confirm table and column names with
get_table_schema(orlist_tables/find_columns) when they are not already in context,
and explicitly not to re-fetch a schema it already has. Guidance only — nothing is enforced
server-side and no call is mandatory.
Fixed
- The
doctorMCP tool re-checks the config file's existence per call, as the CLI already did.
A config deleted or made unreadable after the server started produced a hardfailreading
"connections.json exists but could not be read" instead of the intended skip, "no
configuration found — rundb-conn-mcp setup".
Full Changelog: v0.5.2...v0.5.3
v0.5.2
Closes #12. Two features: whole-setup diagnostics, and making the write preview mandatory.
Breaking / Behaviour changes
-
execute_write_querynow defaults todry_run=true, and the preview is enforced rather
than advisory. A commit (dry_run=false) is rejected unless the identical statement was
dry-run first. A bare call therefore previews instead of committing — an agent that used to
call the tool and have it write will now get a preview back.The grant fingerprints database + SQL + params with a 10-minute TTL.
skip_dry_run=true
exists solely for an agent to attest the user explicitly asked to skip the preview.Gate order is now
mode→ dry-run-first →yolo→user_consent.yolocannot skip the
preview, and nothing can ever make areaddatabase writable.
Added
-
doctor— whole-setup diagnostics, not just connectivity. One engine (doctor.py), two
surfaces:db-conn-mcp doctor [--offline](exit 0 only if nothing fails, 2 otherwise) and a
doctorMCP tool returning{check, status, detail, suggested_action}so an agent can
self-diagnose mid-session. 23 tools total.Six checks, each drawn from a real failure during the 0.5.0 → 0.5.1 upgrade: running server
processes older than the installed package (psutil optional, and it reports its own host
process when stale); a cache-bypassed PyPI version check; per-database connectivity plus a
credential-free listener probe of fallback ports, catching "a different local Postgres
answered my port"; config-schema typos with did-you-mean hints; secrets exposure (POSIX file
mode, git-committability); and injected client entries whose command path no longer exists.The engine never raises — a crashing check degrades to a
failnaming only the exception
type. A poisoned-DSN sweep test asserts no DSN, host, user or password can reach any finding,
including via pydantic validation errors (Rule 6). -
check_databaseUNREACHABLE results now reportfailed_port.
Changed
- MCP-client helpers moved from
cli.pyinto a newclients.py(re-exported for
compatibility), unlocking reuse without a circular import.
Full Changelog: v0.5.1...v0.5.2