Documentation · Operating
Release notes
Images are published to the GitHub Container Registry, and since 2.0.14 to Docker Hub as well:
docker pull ghcr.io/srmadscience/mcpdbwizard:2.0.32
docker pull ghcr.io/srmadscience/mcpdbwizard:latest
Both tags point at the same image, linux/amd64 and linux/arm64. Pin the version tag in
anything that matters — latest moves under you, which is what makes it convenient for a trial
and wrong for a deployment.
And latest does not refresh itself. docker run never pulls an image the machine already
has, so a box that pulled latest once keeps starting that build until someone pulls again — see
:latest does not refresh itself. A pinned version
tag cannot go stale this way, because it never moves.
For what is not fixed, see Known issues.
2.0.32 — 4 October 2026
A name, not a behaviour change. If you are running 2.0.31 there is no need to upgrade.
The feature 2.0.30 introduced as one customer per connection — URL context parameters, read in
PL/SQL with SYS_CONTEXT('MCP', ...) — is now called Context Pinning, and its documentation has
moved to Context Pinning. The old address redirects there.
The console uses the name too. On Design → Service Options the setting is still labelled URL context parameters; its help text now names Context Pinning and links to the page, and so does the note under a server’s endpoint on the Runtime page.
The documentation also now covers which clients work with it: Claude Code, Cursor (whose variable
syntax is ${env:NAME}, and which must be restarted after the URL changes) and the Claude API’s MCP
connector have been verified; a claude.ai custom connector is the wrong place for it, and the page
says why.
Nothing to regenerate, no config change, no database change.
2.0.31 — 4 October 2026
Generation now stops when the database goes away, instead of finishing with half a server. Nothing to regenerate.
If the database stopped answering partway through generating a config — a host that froze, a network that dropped, a database that was shut down — the generator waited out the connection’s read timeout, quietly opened a new connection, and carried on. At every later point that needed the database it did the same thing again, so a single outage cost one ten-minute timeout after another. When it finally finished it reported success, having produced only the classes it reached before the outage. On the console, Start then built and launched a server that was missing tools, with nothing to say why.
It now stops at the first sign that the connection was lost, after one timeout instead of several, and fails with a message saying the database connection was lost mid-run. Batch mode exits with status 3. On the console, Start reports the generation as failed rather than launching an incomplete server; fix the connection and press Start again.
Only generation is affected. The Design page still reconnects as before when you return to it after the database has been away, and servers you have already generated are unchanged.
Upgrading. Pull 2.0.31 and restart. No regeneration, no config change and no database change.
2.0.30 — 2 October 2026
A security fix, and one customer per connection. Upgrade, and regenerate your servers.
Security: error replies no longer carry a Java stack trace
A generated MCP server answered its own transport errors over HTTP — a request with no session, an
unknown session, a message that was not JSON, or a method it rejects before initialize — with the
whole Java exception the MCP SDK had raised: its stack trace, our class and file names, line
numbers, and the Jetty and JDK versions. The body was not even a JSON-RPC error. Every earlier
release did this, and the console’s /mcp/<owner>/<config> proxy passed it to the caller.
These replies are now plain JSON-RPC errors — {"jsonrpc":"2.0","id":null,"error":{"code":…,"message":…}} —
with the same HTTP status as before. Two layers, so you are covered either way:
- The console’s proxy rewrites such replies from any server it fronts, including servers generated by earlier releases. Upgrading the container is enough for anything reached through it.
- Each generated server now rewrites them itself, which covers a server you expose directly. Regenerate to get it.
The status code is kept on purpose. Clients written for MCP’s newest revision, 2026-07-28, first
send server/discover; a server that predates that revision answers with an error, and the
specification uses that reply to tell clients to fall back to initialize. That fallback works as
before.
New: one customer per connection
A config can now declare URL context parameters — names a client puts on its connection URL,
such as ?CUSTOMER_NAME=SUZY%20BISHOP. The generated server sets them in
SYS_CONTEXT('MCP', '<name>') before every tool call, so your PL/SQL and curated SQL can serve one
customer without taking the customer as an argument the model could change. Declare them on
Design → Service Options; the Runtime page shows the URL a client needs. It keeps the agent
in its lane, not the person who configures the client, and a value on a URL must never be a
secret. See Context Pinning.
It needs an Oracle application context installed once per database by a DBA — the scripts are in
db/mcp-context/.
Starting a config that declares parameters against a database without it stops before generating,
and says which script to run. A config that declares none is unaffected and generates exactly as
before, apart from the security fix above.
Also
- A container with no database. Set
MCPDBWIZARD_ORACLE_HOSTto the literalNO_DB_PROVIDEDand the console serves one stub tool,select_from_dual, on/mcpwith no authentication, returning[{"DUMMY":"database connection needed"}]. It exists so a directory or a trial can check that the container speaks MCP before any database is attached. The Design screen becomes an onboarding page in that mode. - The proxy’s access record and the generated server’s
MCP-CALLlog line now name the URL context values a call ran under.
Upgrading. Pull 2.0.30 and restart. Regenerate your servers to get the error-body fix in the servers themselves; until you do, the proxy covers them. No config change and no database change unless you adopt URL context parameters.
2.0.29 — 10 September 2026
Housekeeping. Nothing in the product changed. If you are running 2.0.28 there is no reason to upgrade.
The image now carries an io.modelcontextprotocol.server.name annotation, and that is the entire
release.
The official MCP registry checks that whoever publishes a listing actually controls the image it names: it pulls the image and compares that annotation against the name in the submission. An image without one cannot be listed. Container images are immutable, so 2.0.28 could not be given the annotation after the fact — the listing needed a new image, which is this one.
The generator, the runtime library, the web console and the code they emit are unchanged from 2.0.28. Nothing to regenerate, no config change, no database change.
2.0.28 — 10 September 2026
A TIMESTAMP column inside a ref cursor came back as Oracle’s internal form instead of a time. If any of your routines return a cursor selecting a TIMESTAMP, regenerate — the fix is in the generated code, not in the runtime.
A PL/SQL routine whose OUT or returned ref cursor selected a TIMESTAMP handed the agent this:
"txn_time": { "bytes": "eH4JAww3DS8wG9A=", "length": 11, "null": false, "stream": {} }
That is Oracle’s 11-byte internal representation, base64-encoded — undecodable by a caller, and carrying no date or clock reading a model can use.
The same column read through that table’s own tools was correct, returning
2026-09-03 11:54:12.791682 as text. So one config answered two ways for one column depending on
which tool you asked, and only the cursor answer was wrong. DATE columns were unaffected, which
is what let this go unnoticed: the serializer converted java.util.Date and left everything else
to the JSON mapper, and oracle.sql.TIMESTAMP is a Datum rather than a Date.
Cursors now emit the same JDBC-escape text the table tools already emit, so both surfaces agree — and a timestamp read out of a cursor can be handed straight back to an index-lookup tool, which previously it could not.
What you have to do
Regenerate any config with a routine returning a ref cursor. This is emitted code: an existing generated jar keeps the old behaviour until it is regenerated. Nothing else changes, and no database, config or client change is needed.
Still not converted
TIMESTAMP WITH TIME ZONE and TIMESTAMP WITH LOCAL TIME ZONE inside a ref cursor. Rendering
either needs a database connection the generated helper does not have, and no fixture exercises
that shape — so it was not fixed blind. Plain TIMESTAMP, the common case, is fixed. See
Known issues.
Verified across all six supported Oracle versions — 12.1, 18c, 19c, 21c and two 26ai instances.
2.0.27 — 7 September 2026
A republish of 2.0.26. There is no functional change, and nothing here for you if you are already running 2.0.26 outside AWS Marketplace.
2.0.26 is the release that gave every config an owner — see below, and read it before upgrading, because every MCP client URL moves.
This version exists because of how AWS Marketplace handles a listing’s deployment resources. A version’s CloudFormation template is attached in a second step after the version is added, and that field is frozen once a product is public — so 2.0.26 went out on the Marketplace with its template missing, while its instructions told buyers to deploy it. 2.0.26 has been restricted there and this release replaces it, carrying the template from the start. Container image tags cannot be reused once submitted, which is why it needs a version number of its own rather than a corrected upload.
Outside AWS Marketplace — Docker, your own server, any other cloud — 2.0.26 and 2.0.27 are the same build, and the pull for either works exactly as before.
2.0.26 — 7 September 2026
Every config now belongs to an account, and its MCP address changes with it. Read the upgrade note before you update: every client URL moves.
A config used to be named just payroll, and that name belonged to the whole installation. The
first person to use it took it from everybody else — and the way the second person found out was
being told they had no access to a config they had never seen, which also told them it existed.
Configs are now named owner/config. Two people can each have a payroll; neither can write
into the other’s.
before http://host:8080/mcp/payroll
after http://host:8080/mcp/alice/payroll
You still type only the name. The owner is whoever is signed in, and you can create configs in your own namespace only — an admin can write anywhere.
What you have to do
Update the URL in every MCP client. The old address is not redirected, and deliberately so:
several people may now have a payroll, so there is no correct config to send a caller to and
guessing would hand them somebody else’s. It answers with a message naming the new shape instead of
a bare 404, which an MCP client cannot tell from a server that was never started.
Everything else happens by itself, once, when the new version first starts. Your configs move into place, their SQL and their generated servers move with them, and access grants are rewritten. Nothing needs regenerating. Every config is logged with its new URL as it moves — that list is what you need to edit your clients:
Migrating 3 config(s) from the flat layout in /data to /data/configs
payroll -> alice/payroll its MCP URL is now /mcp/alice/payroll
Who ends up owning what: a config that exactly one account was granted goes to that account, since that grant was how the old version recorded who saved it. One that nobody was granted — an admin’s own — or that several people shared goes to the admin, who can hand it on afterwards.
The access page is simpler
Granting used to happen twice: once automatically when you saved a config, and once when you shared it. Only sharing is left, because a config’s name now says who owns it. Every tick on the Access page is one account being given somebody else’s config, and your own show as owner rather than as a checkbox that did nothing.
Rolling back
There is no automatic way back. If you return to an earlier version you will have to move the
configs out of configs/<owner>/ by hand, and the start-up log above is the record of where each
one came from.
If a config cannot be moved — the only realistic cause is that its name is already taken in the new layout — both copies are left exactly as they are and the start-up log says so. Nothing is overwritten and nothing is deleted; sort it out by hand and restart.
2.0.25 — 5 September 2026
A rebuild of 2.0.24 from a clean tree. There is no functional change.
2.0.24 removed the last default password from the product. This release contains the same code, built from scratch with no reused build output, and published as a new version because container image tags cannot be reused once submitted.
If you are already running 2.0.24 there is nothing here for you. If you are deploying fresh, use this one.
2.0.24 — 5 September 2026
There is no default password any more. Not on AWS, not anywhere.
On its first start this installation generates a password of its own, writes it to
initial-admin-password in the config directory, and the sign-in page tells you exactly where to
look. Read it, sign in, choose your own — the file is deleted the moment you do.
docker exec mcpdbwizard cat /data/initial-admin-password
If you would rather choose the password yourself, set ADMIN_INITIAL_PASSWORD as before. Either
way you are required to change it before the console will do anything else.
Why this replaces the last two releases’ approach
2.0.22 made the initial password the EC2 instance ID; 2.0.23 extended that to the ECS task ID for Fargate and EKS. Both kept a built-in password in the product and merely arranged for AWS deployments not to reach it. That is a weaker thing than it sounds: the value was still there, and each release had to learn about one more platform to keep steering around it.
Generating a password needs to know nothing about where it is running, so the AWS detection is gone along with the default. One rule, everywhere.
If you are upgrading
Nothing changes for an existing installation. Your accounts are in users.properties and are
untouched; a password is generated only when an admin account is first created. New installations,
and anyone starting with an empty config volume, get the flow above.
2.0.23 — 5 September 2026
The per-deployment first-login password now covers ECS and Fargate, not just EC2.
2.0.22 made the admin account’s initial password the EC2 instance ID, with a forced change at first login. That was the right behaviour and it reached too few places: a Fargate task has no instance ID at all, and an EKS pod whose cluster blocks the instance metadata service from its pods — ordinary hardening, not an exotic setup — cannot read one either. Both are unmistakably AWS, both answered “not on AWS”, and both then fell back to the built-in default password.
Since this product is offered on Amazon ECS or EKS, that was not a corner case. It was most of what the listing offers.
The question the code asks is now “am I on AWS?”, and the EC2 instance ID is only one of the ways to answer it. Where there is no instance ID, the ECS task ID is used instead — and the forced change applies exactly as before. If you deploy on EC2, nothing changes: the instance ID still wins when both are available.
Still AWS only
Unchanged, and deliberately so: a standalone docker run, a docker-compose deployment, or another
cloud behaves exactly as it always has. ADMIN_INITIAL_PASSWORD still seeds the account if you set
it.
The new check is cheaper off AWS than the EC2 one, too. ECS puts a metadata variable into every task it starts, so its absence is a complete answer — there is no address to contact and nothing to wait for.
Raised by the AWS Marketplace listing audit, which found the gap that 2.0.22 left open.
2.0.22 — 5 September 2026
On EC2, the admin account’s first-login password is now the instance’s own ID, and it must be changed before the console will do anything else.
There is no longer a default password on that path, and no password parameter in the CloudFormation
template — so nothing to leak through the CloudFormation console, a change set, or a shell history.
Sign in as admin with the EC2 instance ID (i-…) and you are taken straight to a change-password
form.
The instance ID is deliberately not treated as a secret — anyone who can see the EC2 console can read it. It is a bootstrap credential, and its safety is the forced change.
This affects AWS deployments only
A standalone docker run, a docker-compose deployment, or another cloud is unchanged:
ADMIN_INITIAL_PASSWORD still seeds the account if you set it, and the behaviour without it is what
it always was. The change is scoped by asking the EC2 instance metadata service whether there is an
instance ID at all; where there isn’t, nothing moves.
The bug behind it
Supplying ADMIN_INITIAL_PASSWORD switched the forced password change off. The account was
marked “not default”, and the forced-change check keys on exactly that flag. Since the
CloudFormation template always supplied a password, the forced change could never fire on an AWS
deployment — the machinery was present and correct, and disabled by the deployment that most needed
it. On EC2 a freshly seeded admin now always requires a change, however the password was chosen.
Raised by the AWS Marketplace listing audit.
Also in this release
Two CloudFormation template fixes, both of which prevented the template deploying at all:
- A security-group rule description contained
<and>, which EC2 rejects. The stack failed on its first resource and created nothing else. - The EFS access point declared group
1001while the container runs as group999, so every task died withfailed to chown … operation not permitted— after all the infrastructure had built successfully, which made it look like a slow deployment rather than a permissions mismatch.
Neither is detectable by aws cloudformation validate-template; both were found by deploying it.
2.0.21 — 4 September 2026
Four CVEs in bundled dependencies, and a security-focused release.
| CVE | Component | Was | Now |
|---|---|---|---|
| CVE-2024-50379 | Apache Tomcat — RCE via the write-enabled Default Servlet | 10.1.31 | 10.1.55 |
| CVE-2024-56337 | Apache Tomcat — incomplete mitigation for the above | 10.1.31 | 10.1.55 |
| CVE-2025-24813 | Apache Tomcat — RCE via partial PUT | 10.1.31 | 10.1.55 |
| CVE-2026-42581 | Netty — HTTP request smuggling in HttpObjectDecoder | 4.1.118 | 4.1.137 |
The Tomcat three arrive through the embedded server the web console runs on. The Netty one arrives through the optional AWS SDK that backs the S3 audit sink, and is present whether or not you use that sink, because the jars ship in the image either way.
Nothing in MCPDBWizard’s own code was affected, and no generated server was exposed by the Tomcat issues — a generated MCP server runs on Jetty, not Tomcat. What was exposed is the web console, which is the component that listens on 8080.
Delivered by moving to Spring Boot 3.5.16 (from 3.3.5) and AWS SDK 2.54.12 (from 2.31.7), rather than by pinning individual versions. The 3.3 line is out of open-source support, so pinning would have bought these three fixes and no runway.
Pull the new image. These are dependency versions inside the container, so nothing you generate or configure changes — but a container still running 2.0.20 still has them.
The Runtime page no longer asks you to run an empty script
A config with no collection parameters was still offered Download extraObjects.sql, described as
“the TYPE definitions this config’s collection parameters need” — for a file containing one
comment and one blank line, describing parameters that do not exist. Reported as
issue #1.
The page now asks whether the DDL actually declares a type, rather than whether the file is non-empty. Configs that genuinely need those types are unaffected, and the download still appears for them.
2.0.20 — 4 September 2026
TIMESTAMP values were being stored to the nearest second.
That is data loss on write, and it affected every generated program with a TIMESTAMP column:
inserts, updates, deletes, row locks and every keyed lookup. A TIMESTAMP(6) column given
2026-01-01 00:00:00.25 stored 2026-01-01 00:00:00.0.
The cause is a reasonable rule applied to the wrong type. A TIMESTAMP bind parameter was declared
java.util.Date, and the runtime library strips the sub-second part from a java.util.Date
deliberately: an Oracle DATE holds whole seconds, so keeping the fraction there would write
rows that a later equality search could never find. Correct for DATE. Wrong for TIMESTAMP,
which has the precision the column was chosen for.
What people met first was a search that returned nothing. A row whose timestamp came from
SYSTIMESTAMP carries microseconds; the value bound to look it up had been rounded to the second,
so an exact-match lookup on a TIMESTAMP column found no rows at all — successfully, with no error
and nothing in the log.
PL/SQL procedure and function parameters were never affected: they already bound through the correct path.
TIMESTAMP columns came back null from multi-row reads
Separately, and in generated code of every vintage: a manager method returning an array of rows
never populated its TIMESTAMP columns. Index lookups, foreign-key child lookups and every other
multi-row read returned the column as null, while getByPk on the very same row returned the value
— so the two disagreed about one row, which reads like a fault in the query rather than a missing
assignment.
TIMESTAMP WITH TIME ZONE and TIMESTAMP WITH LOCAL TIME ZONE columns were affected in the same
way, and are fixed with it.
An MCP server with a TIMESTAMP in a secondary index would not compile
If a table exposed over MCP had a TIMESTAMP column inside a unique key or an index, the generated server failed to build:
error: no suitable method found for getChildByIxUrtDelIdx(...)
An index lookup is the one MCP surface that calls the generated manager with ordered scalar
arguments rather than a row object, and it was passing the row’s String form where the manager
declares java.sql.Timestamp. Such a lookup now accepts either the yyyy-mm-dd hh:mm:ss.ffffff
form the other tools for that table emit, or ISO-8601 — a server should take back a value it has
just handed out.
Upgrade notes
Regenerate. The fixes are in the emitted code, so existing generated sources keep the old behaviour until they are regenerated.
Values already written stay truncated. This corrects what is written from now on; it cannot recover a fraction that was discarded before.
One source-level change, for TIMESTAMP columns only. The generated statement classes now hold
such a parameter as a String and expose:
| Setter | Before | Now |
|---|---|---|
setParam<Col>(java.sql.Timestamp) | yes | yes |
setParam<Col>(String) | yes | yes |
setParam<Col>(oracle.sql.TIMESTAMP) | no | yes |
setParam<Col>(java.util.Date) | yes | gone |
setParam<Col>(long) | yes | gone |
Code that passes a java.sql.Timestamp is unaffected. Code that passed a plain java.util.Date to
a TIMESTAMP column will no longer compile — pass a java.sql.Timestamp instead, which is what
keeps the fractional seconds. Row classes, manager signatures and DATE columns are all unchanged.
2.0.19 — 2 September 2026
A PL/SQL index-by table of records can be read back, and keeps its subscripts.
A routine returning an index-by table whose element is a record failed with ORA-06532: subscript outside of limit. An index-by table is keyed by arbitrary BINARY_INTEGERs; the SQL
nested table it is copied into on the way out is keyed 1..COUNT. The generated block indexed the
second with the first, so any collection not keyed densely from 1 was rejected:
| Index-by keys | Before |
|---|---|
Dense from 1 — what BULK COLLECT produces | worked |
Base 0, e.g. t(t.COUNT) := rec | ORA-06532 |
| Sparse, e.g. 1, 5, 9 | ORA-06533 |
| Negative | ORA-06532 |
The copy is now densified. The same fix covers a nested table that has had DELETE(n) applied,
which is sparse in exactly the same way and had no guard against it at all.
Your subscripts survive the round trip. Each element of a record collection now carries its
PL/SQL subscript in a MCPDBWIZARD_POS attribute, so a base-0, sparse or negative collection comes
back with its keys intact and goes back in at the same keys instead of being silently renumbered. A
collection you build yourself, rather than one you read back, still binds densely — there is nothing
to preserve.
Re-run extraObjects.sql after regenerating
Carrying the subscript changes the generated Oracle TYPEs, so generated code will not bind against
the types from an earlier release. Regenerate, then run the extraObjects.sql your generation
wrote.
Until you do, the first bind reports
Oracle TYPE … does not match the definition this code was generated against
naming the type. That check is deliberate: a stale type would otherwise bind wrong values quietly.
Note the file has no / terminators, so SQL*Plus buffers it — run it through a client that
executes each statement, or add them.
Types the database owner declared are never touched. Only the types MCPDBWizard generates change.
Also in this release
- A known issue on Oracle 12c, found by the fixture written for the fix above: a routine that takes a record collection in and returns one out will not bind there. 12c only, and only that combination — see Known issues 11 for the workaround.
2.0.18 — 2 September 2026
A server’s instructions now name the operations each table really exposes.
The sentence describing tables was a fixed phrase, emitted whenever a config had any table at all:
Exposes row CRUD on table(s) … (get_by_pk / insert / update / delete)
It said that however the per-table CRUD flags were set, so a config whose every table was read-only still announced four write operations per table.
The tools were always correct — an operation you do not select has no tool emitted at all, so
nothing could be written that should not have been. What was wrong was the description, read by the
one party that acts on it: a model reads instructions before any tool list, so it would call
insert on a read-only table and get unknown tool back. And because a config’s selections are
its authorization, the sentence misstated the server’s security posture even though it could not
weaken it.
Tables are now grouped by the operations they share, so twelve read-only tables read as one clause naming twelve tables rather than twelve clauses:
Exposes row operations on table(s) BOOKINGS (get_by_pk / insert / update / delete) and table(s) AIRCRAFT, AIRPORTS (get_by_pk), rows crossing as JSON objects keyed by column name.
A config where every table is full CRUD reads exactly as it did before, to the byte. That is the common case, and a release should not rewrite the instructions of a server whose exposure has not changed. The server’s one-line description follows the same rule: “row CRUD” only where that is true of every table, “row access” otherwise.
2.0.17 — 2 September 2026
A DATE inside a record now crosses exactly as a DATE parameter of its own does — and three of the four differences were silent.
Known issue 2
reported one symptom: a record field refused a bare 1980-01-01 that a scalar date parameter
accepted. That is the half that failed loudly. Measuring the rest found three more, none of which
raised an error:
- a zone offset was discarded —
1980-01-01T09:30:00+05:30was read as 09:30 in the server’s own zone, five and a half hours out; - fractional seconds were discarded;
- an impossible date rolled forward —
2003-13-45arrived as2004-02-14.
Check your data if you have been sending offsets into record parameters. Values written by a generated server before 2.0.17 may be wrong by the offset that was sent, and nothing in the reply would have shown it.
The cause was one line, and it is worth naming because it looked handled. The record mapper was configured with the ISO pattern the rest of the product uses. Handing a JSON mapper a pattern gets none of the parsing wrapped around it: the underlying formatter stops at the end of its pattern and ignores whatever follows, and it is lenient about impossible values. So the fix that landed for scalar dates in 2.0.0 never reached record fields. Both directions now go through that one parser.
A TIMESTAMP field inside a record also gains back the fractional seconds it was dropping on the
way out. An epoch millisecond number is still accepted, as it always has been.
A config’s own MCP instructions were discarded when it exposed a duality view.
The server’s instructions are assembled clause by clause — the author’s own text first, then the
inventory of what the config exposes. The duality-view clause replaced what was in hand instead of
adding to it, so on any config with a view the author’s words were built and then overwritten.
It failed in the worst way available: the console accepted the text, stored it, and read it back onto the tab it was typed into, so every surface you could see agreed it had been saved. The only place it was missing was what the server told a model. Configs without a duality view were never affected, which is why it survived — every other clause appends correctly.
Verified across the estate, all 41 configurations regenerated against each of six Oracle instances from 12.1 to 26ai, with no test failures on any of them.
2.0.16 — 1 September 2026
Each JVM is now given its heap explicitly, instead of taking a share of the container.
The image left every JVM on the default -XX:MaxRAMPercentage, so the web app, the forked generator
and every running MCP server each independently sized a heap at 75% of the whole container
limit. One process alone is fine; three are a promise the container cannot keep, and the way it
breaks is that the kernel kills whichever process has grown largest — usually the web console, and
never with an error that says memory.
Heaps are now set per role: JAVA_OPTS for the web app (default -Xmx1g) and
mcpdbwizard.runtime.java-options for each generated server (default -Xmx512m), with a separate
mcpdbwizard.runtime.generator-java-options for the generator, which is the one that genuinely
wants more. Budget the container as
limit >= (web heap + ~300m) + servers * (server heap + ~200m)
the trailing term being the non-heap footprint -Xmx does not cover. At the defaults, 4 GB holds
the console and about four concurrent servers — see
Set a memory limit.
Raising the container limit no longer raises the heaps with it, which is the one behaviour
change to know about. If you had sized a container generously and were relying on the percentage to
follow, set JAVA_OPTS to what you want.
A generated server now warns at start-up when its tool count can outgrow Oracle’s open_cursors.
A generated DAO holds one open cursor per distinct tool for the life of the session — statement
reuse working as intended — so a config with more tools than open_cursors (default 300) eventually
meets ORA-01000 on whichever tool is used next. It reads as intermittent, because a tool that has
already been called keeps working and only a previously-unused one fails.
The server now reads open_cursors from V$PARAMETER before serving anything and says what it
found: a warning at 70% of the limit, an error at the limit, and where the account cannot read the
view, that the limit is unknown and could be exceeded. Oracle’s default of 300 is quoted in that
message and is never used as a limit — guessing one would be worse than saying so.
Nothing is capped and nothing is retried: this is a database setting, and the fixes are to raise
open_cursors or to split the config. The new Oracle parameters page
covers both, plus the query that shows what a running server is holding.
2.0.15 — 31 August 2026
MCP tool descriptions are now written on the tool listing, beside the text they replace.
They used to have a description link next to every selected object on the Procedures, Tables,
Sequences and SQL Statements tabs. That page had a flaw it could not be fixed out of: it could not
show you what you were replacing. Every generated description but a sequence’s is built from
metadata that only exists during generation — a table’s crossable column list, a routine’s parameter
tuples, a duality view’s field list — so the page described what the default would contain instead
of showing it.
They are now edited on Design → Service Options, in the Tools this config exposes panel: one
box per tool, with the description that tool publishes today printed underneath it. That panel is
the generated server’s own answer to tools/list, so what you read is what a model reads.
Leave a box empty and the generator writes the description, from the data dictionary — the normal case, and usually the better one, because it stays correct when the schema changes. Clearing a box you have written in goes back to the generated text.
The cost, stated plainly: a config must be generated once before its descriptions can be edited. Which tools exist is decided during generation, so before that there is nothing to describe. The panel says so rather than showing an empty form.
A description written on a PL/SQL routine could be silently lost, and this release fixes it.
The editor accepted it, reported it saved, and stored nothing. A routine with no package or no
overload is spelled two different ways by two pieces of software that are both right: the generator
writes the literal null — because that is what a .pb2 file actually contains — while a config
authored in the web console writes it empty. The two described the same routine and compared
unequal. Matching now happens on a normalised key, so either spelling finds it.
Underneath it was the worse half: the code that stores a description returned nothing at all when it could not place one, so the console counted a write that never happened. It now reports what it could not place instead of counting it as saved.
Tables, sequences and SQL statements were never affected — their keys have no optional segment.
2.0.14 — 29 August 2026
Generated MCP servers no longer advertise tools/list_changed, a notification they have never
been able to send.
Every generated server answered initialize with "tools":{"listChanged":true} and had no code
path able to send one — the tool list is built once at construction and never changes. A client
that believes listChanged has no reason to poll tools/list again, because it expects to be
told.
False is the true answer, not a retreat from a feature. Curation happens at generation time: an operation you did not select has no tool-spec method emitted at all, so a server’s tool set is fixed for the life of its process by design. Changing it means regenerate-and-restart, which a client re-initializes against.
The image is also published to Docker Hub from this release, from one build tagged for both registries, so the two are the same manifest digest by construction rather than by two builds. Prefer ghcr.io where you have no reason not to: Docker Hub rate-limits pulls per source IP, so a CI runner or an office NAT can exhaust the allowance without any one person pulling much.
2.0.13 — 28 August 2026
Two writers could fill the config volume. The audit trail’s MCP_AUDIT_FILE_MAX_BYTES caps
one directory, and a trail directory takes exactly one writer — so every generated server got its
own and each independently took the 512 MB default. The advertised cap was 512 MB per config,
roughly 10.5 GB across the default port range. MCP_AUDIT_TRAIL_MAX_BYTES is now a budget for the
whole trail, divided among its writers.
Its source drop went out with 2.0.14, not with the image. The 2.0.13 image was published to both
registries and its open-source export was never run, so there is no v2.0.13 tag in the source
repository — 2.0.14’s export carries both versions. Recorded here rather than quietly corrected:
the image exists, and anyone reading provenance for it should know where the matching source went.
2.0.12 — 28 August 2026
A RAW or DATE field inside a record now tells a caller how it crosses.
Both were published as a bare {"type":"string"} — true, and useless. It does not say “send
base64”, and it does not say the T is required, so the only way to learn either was to be
rejected. A scalar parameter of the same Oracle type has always said so, so this is the existing
contract reaching one level further in rather than a new one.
The RAW case was worse than a rejection, which is what makes it more than a schema nicety. Hex is the spelling a person reaches for with RAW, and hex and base64 are both plain strings — so sending hex was not refused. It decoded into different bytes and the procedure ran. The note therefore says “base64, NOT hex” with an example, rather than just “base64”.
Also in this release: each test class gets its own config directory, RUN_ON_START is documented,
and the undocumented MCPDBWIZARD_RUN_THIS_FILE escape hatch is withdrawn.
2.0.11 — 27 August 2026
Uploading a config now saves it, and a deployment can adopt one from a file at boot.
Upload used to load the JSON into the editor and say “Use Save to keep it”. So an uploaded config was invisible on the Runtime page, absent from the access matrix, and gone at the next restart — while the Design page showed it as the current config, so it looked present in the one place anybody was looking. Somebody uploading a config file is stating that they want that config here. The save path also does more than write the file: it copies the SQL library into the config’s own scope and stamps the deployment’s Oracle connection.
/.well-known/** answers without credentials.
Discovery happens before a client has credentials, by construction: /.well-known/* is where an MCP
client looks to find out how it is meant to authenticate, so putting it behind authentication is
circular.
The failure this prevents is worse than a 401. Under form login the entry point answered a probe
with a 302 to /login; the client followed it, got a page of HTML with 200 OK, and reported a
parse error — so “this deployment does not offer OAuth” arrived as a malformed-response bug
several layers from its cause.
2.0.10 — 27 August 2026
A container could be entirely healthy and still refuse every Prometheus scrape.
The port was published, docker ps showed 0.0.0.0:9464->9464/tcp, the child had
MCP_METRICS_PORT in its environment — and nothing answered, with no line in any log saying why.
The exporter was listening on 127.0.0.1 inside the container, and Docker forwards a published
port to the container’s bridge address, not its loopback.
The fix is in the runtime rather than in the exporter’s default, and the asymmetry is deliberate: that default is right for the case it was written for, because the same class runs as a plain library in somebody’s own JVM on a real host, where switching metrics on must not be the act that puts a server on a network.
Note this is the opposite of what the same code does for the MCP port, where loopback does not deny
access but channels it through the /mcp/{config} proxy — the only component that knows who is
calling.
The three metrics settings are also now documented where a Docker user will actually find them.
2.0.9 — 27 August 2026
A sparse index-by OUT collection no longer fails with ORA-01403. This affects generated DAO
code, not just MCP — regenerate to pick it up.
An index-by table is sparse by nature: PL/SQL lets a routine assign element 1 and element 7 and
nothing between. The generated code walks such a parameter from first to last index, so a gap made
it read an index that is not there — ORA-01403: no data found, raised from inside the emitted
block, naming neither the parameter nor the gap.
Which parameters were affected. Only those whose values are converted on the way out, which is where the guard was missing:
| Element type | Before |
|---|---|
NUMBER that does not fit the numeric slot — e.g. number(30,15) | failed |
RAW | failed (fixed in 2.0.6) |
DATE, TIMESTAMP, TIMESTAMP WITH [LOCAL] TIME ZONE | already correct |
VARCHAR2, and a NUMBER that binds numerically | never affected — no conversion loop |
So the realistic case is a high-precision NUMBER collection with gaps in it. If you have one and have never seen this, your routine returns a dense collection and always did.
Positions are preserved. Values come back at the indexes the routine set them at, with the gaps empty — they are not compacted to the front. Verified on Oracle 12c through 26ai.
How this was found, since it says something about the rest. The RAW case was fixed in 2.0.6 by
comparing the code against its neighbours, with no test that failed without the fix — nothing in the
test schema returned a sparse collection. Adding a fixture that does turned up a second unguarded
type, and now holds both in place: six boxes across five Oracle releases, guard removed → the error
reproduces, guard restored → it does not.
Also in this release: the documentation now explains that docker run does not re-pull
:latest, so a machine that pulled it once keeps starting that build until someone pulls again.
2.0.8 — 27 August 2026
The console shows which version it is running — on the login screen, and on the right of the banner once you are signed in.
Two places because they answer for two different people. The banner tells whoever is already working here which build they are on. The login screen tells somebody handed a URL who is deciding whether that box has a fix in it — and that person may not have an account, so putting it only behind a login makes “what is deployed here?” require one, which is backwards for a fact the image tag already discloses.
The number is read from the build itself, so it cannot drift from the release. A console that
confidently names the wrong version is worse than one naming none, because the only reason to read
it is to decide whether a fix is present. A build that stamped no version shows dev rather than
guessing something release-shaped.
Nothing else changed — no generator, no generated server, no proxy.
2.0.7 — 27 August 2026
A console release. Nothing in the generator, a generated server or the proxy changed — the tools your configs publish are identical to 2.0.6. What changed is what the console tells you when something is not configured, which is where two people lost time.
The setup instructions now show a command that works. Launching on
Docker gave a docker run with no Oracle settings and said to add them from
another page, which held a bare -e fragment with no command around it. Following either produced
a console that starts, passes its healthcheck and refuses every Design page. The minimum is now
the whole command. Also documented: passing values already in your shell with -e NAME (no
=value), which keeps the password off the command line and out of shell history; that
MCPDBWIZARD_ORACLE_PORT is the one you can omit; and the leading-/ on the SID, stated where it
bites rather than three sections below it.
The “Oracle is not configured” message names the password. It listed the host, SID and user
and said nothing about DB_PASS, so an operator who set exactly the three things they were told to
set got a login failure — which reads as a wrong password rather than as one nobody asked for. The
same page also decided whether a restart was needed from a check that ignored the password, so a
deployment missing only DB_PASS was told to reload a variable a running process cannot be given.
Start-up warns once per missing variable. One line each, naming the variable, what it is for and an example, so a line can be read on its own — instead of one sentence listing them all, which is the shape most likely to be truncated, and losing its tail loses a whole setting.
WARN Oracle is not configured: 4 required setting(s) are not set.
WARN MCPDBWIZARD_ORACLE_HOST is not set - the database host, e.g. db.example.com
WARN MCPDBWIZARD_ORACLE_SID is not set - the SID, or /SERVICE for a pluggable database, e.g. /FREEPDB1
WARN MCPDBWIZARD_ORACLE_USER is not set - the Oracle account to connect as
WARN DB_PASS is not set - the password for that account, or DB_PASS_FILE naming a file that holds it
Admin → Audit stops understating what it protects. The page said flatly that the write-ahead
spool is not encrypted. That has been optional since 2.0.5 — set MCP_AUDIT_SPOOL_KEY and it is
AES-256-GCM — and the page now reports what your deployment actually does rather than asserting a
constant, naming the variable either way. It matters most at MCP_AUDIT_LEVEL=VALUES, where the
same paragraph tells you the queue holds tool arguments and responses.
2.0.6 — 26 August 2026
Every Oracle scalar type now crosses an index-by collection. DATE, TIMESTAMP and RAW
elements of a PL/SQL index-by table used to remove the entire routine from the tool list. The
gate is per routine rather than per parameter, so one such argument hid every operation that routine
would have had — no error, no degraded tool, just an absence. TIMESTAMP WITH [LOCAL] TIME ZONE
came out of that gate in 2.0.1; the remaining three follow here, and the gate is now empty of scalar
types.
If you selected a routine in the config and could not find its tool, this was the likeliest reason.
It is worth re-reading tools/list after upgrading: the tools appear with no change to your config.
What each element accepts, published in the tool schema rather than left to be guessed:
| Element | Crosses as | Example |
|---|---|---|
DATE | ISO-8601 date, time optional | 1990-01-01 or 1990-01-01T09:30:00 |
TIMESTAMP | ISO-8601, up to 8 fractional digits | 2019-03-01T14:25:36.123456 |
RAW | base64, not hex | 3q2+7w== for the four bytes DE AD BE EF |
Two of those carry a caveat the description now states out loud rather than leaving you to find it.
An Oracle DATE has no sub-second precision, so fractional seconds are dropped — sending them
is neither honoured nor an error, which is exactly the silent middle ground a caller cannot discover
for themselves. And a RAW is shown in hex by SQL*Plus, by a DESCRIBE, and by most tools you
will have met one in — but MCP has no binary type, so it crosses as base64. Sending hex is not
rejected, because it cannot be: DEADBEEF is also valid base64 and decodes cleanly to four
completely different bytes. Read the parameter description, which says so.
A sparse index-by of RAW no longer risks a NO_DATA_FOUND. The generated PL/SQL walked such a
collection from first to last index without checking that each one exists, where the DATE and
TIMESTAMP paths always had. PL/SQL lets a routine assign element 1 and element 7 and nothing
between, so a gap raised an error naming neither the parameter nor the gap. This affects generated
DAO code as well as MCP tools, so regenerate to pick it up.
Nothing else changed. No new configuration, no schema changes, and no change to any tool that already worked — the release adds tools that were previously absent and fixes the loop above.
2.0.5 — 26 August 2026
Another small one, and worth saying so plainly: nothing in the generator, the proxy or a generated server changed. The only new code is a load-testing client that ships alongside them. If you are happy on 2.0.4, the reason to move is the image metadata rather than anything the product does.
The image now says what is in it. Every published image already carried a BuildKit provenance record — that has been on by default since 2.0.0, and is not new here. What is new is an SBOM listing the contents as SPDX, one document per platform, which no earlier image has. It reads straight off the registry, without pulling anything:
docker buildx imagetools inspect ghcr.io/srmadscience/mcpdbwizard:2.0.5 --format '{{ json .SBOM }}'
The question this exists for is the one that arrives on the morning of the next Log4Shell — is the bad version in your image? — and it is now a search you can run yourself in a minute instead of a mail to us. Verifying the image has the commands, including how to search the SBOM for a named package.
And it is honest about what it is not. The image is not signed: there is no cosign or Sigstore signature, so nothing here identifies the publisher. The provenance is self-attested, produced on a developer machine by the same party that publishes the image, which makes it a record rather than independent evidence. It is genuinely useful for what is in it and what was it built from, and it should not be quoted as more than that. The docs page says so at greater length, deliberately.
Base images are pinned by digest. Both stages of the Dockerfile name an exact digest rather
than a floating tag, so two builds of the same commit contain the same operating system — and the
provenance reports what was chosen rather than whatever the upstream tag happened to point at that
morning.
You can load-test your own server without an LLM. A new client ships in the image and can drive any published tool at a controlled rate for a bounded time — the RPC level, with no model in the loop, so a run is repeatable and costs nothing:
docker exec mcpdbwizard java -cp '/app/lib/*' com.mcpdbwizard.loadtest.McpLoad \
--url http://127.0.0.1:8080/mcp/<config> --token <api-token> --list
--list prints every tool the server publishes with its input schema and a skeleton workload entry,
which is the starting point for a real run: --workload work.json --for 5m --rate 200 --threads 8.
It works against the proxy, as above, or against a generated server’s own port.
It reports latency twice, and the second number is the one that matters. Service time is send-to-receive; scheduled latency is from the moment a call was due to start. Drive a server past what it can sustain and service time stays flat and reassuring while scheduled latency climbs into seconds — the queue is the whole answer, and a tool reporting only the first would tell you everything is fine. It also states achieved rate against target, so a run that fell behind says so.
A workload entry can carry "check": "unique", which verifies that every call returned a different
value and that the values span exactly as many numbers as there were calls. That is what separates a
fast server from a broken one returning a cached answer, and a throughput figure alone cannot.
Two notes from using it. Prefer --for over a call count for anything long: throughput climbs
steeply for the first minute while the JVM compiles, the pool grows and Oracle caches cursors, so a
count sized from a five-second sample is measuring warm-up — which is also what --warmup 30s is
for. And pass -Dlogback.configurationFile= pointing at a quiet config, or logback defaults to
DEBUG and buries the results under a line per call.
Two more known issues, both documented rather than fixed: a DML SQL statement of your own cannot hand back a generated key, and there is no support for TNS aliases, wallets, TLS to the database, RAC or SCAN. Both are on Known issues with what a caller actually sees and the way round each.
2.0.4 — 25 August 2026
A small release, and worth saying so: one metadata fix in the image and one line of copy off the Runtime page. Nothing in the generator, the proxy or a generated server changed, so there is no reason to move unless you read image labels.
The image now describes itself correctly. It carried four OCI labels and inherited two more
without meaning to. eclipse-temurin is Ubuntu-based, Ubuntu’s image sets title, description,
created and version, and a label that is not overridden is inherited rather than absent — so
every release up to and including 2.0.3 announced
org.opencontainers.image.version = 26.04
org.opencontainers.image.created = 2026-08-17T09:02:45
Ubuntu’s release number in the field tooling reads to identify ours, and a build date eight days
before the image was pushed. Both now say what they should, alongside six more: vendor,
licenses, url, documentation, title and description. The four that vary per build are
also published as manifest annotations, so a registry UI can read them without pulling.
docker inspect --format '{{json .Config.Labels}}' ghcr.io/srmadscience/mcpdbwizard:2.0.4
licenses reads LicenseRef-Proprietary. The repository has no single licence — the generator
and the runtime library under app/ are Apache-2.0, the console under web/ is proprietary — and
the image ships both halves, so the artifact as distributed is proprietary. SPDX has no registered
identifier for all-rights-reserved, and naming Apache-2.0 here would have overstated the grant.
Off the Runtime page: a footnote telling you a config’s MCP server option must be YES and its target JVM at least 17. That is a Design-tab concern stated on the wrong page — by the time a config is listed on Runtime the question is settled, and a start that fails for either reason says so at the moment it fails.
2.0.3 — 25 August 2026
You can see a running server’s tools without leaving the console. The Runtime page now shows
each server’s real tools/list response — the payload itself, on the page, not behind a control
you have to know to click. A long list is capped with the whole thing offered as a file, and the
same view is available from the command line with --list-tools. Servers started automatically at
boot are listed too, and a server generated before this existed is simply not asked rather than
being reported as broken.
Start-up says what is not configured. A container missing a required Oracle variable used to start, serve the console and fail only when something touched the database. It now logs a single line naming exactly which of the host, SID, user and password are unset, and what will and will not work until they are. It still starts — deliberately, because the console has to be able to stop a running server during an outage — but it no longer does so silently.
Prometheus service discovery at /sd/mcp, so a scrape config can find the running servers
instead of being edited each time one starts. The metrics port is now published in the hand-written
docker run in the docs as well as in the compose file, and the docs say plainly that each MCP
server needs a metrics port of its own.
The Runtime page no longer returns a 500 while a config is being regenerated.
Also in this release: the Descriptions tab moved onto the same Design model as the other tabs, and a demo hotel-booking schema is checked in so the quickstart has something to point at.
2.0.2 — 21 August 2026
The object list says how many descriptions an object carries, so a table with four written and one missing is visible at a glance rather than by opening each one.
A foreign-key child lookup names the child table in its description. It previously described the relationship without saying which table came back, which made two lookups on the same parent hard to tell apart.
2.0.1 — 21 August 2026
Zoned timestamps cross MCP, with their zone. A PL/SQL index-by collection of
TIMESTAMP WITH TIME ZONE or TIMESTAMP WITH LOCAL TIME ZONE used to remove the entire routine
from the tool list — the gate is per routine, so one such parameter hid every operation. Two things
were wrong underneath: the value crossed JDBC through a text shuttle whose conversion masks carried
no zone at all, so an offset was rejected outright and a zone-less value came back with the
server’s zone silently applied. Both directions now use a zone-aware mask, verified live on the 12c
and 23ai lines preserving both +05:30 and Asia/Calcutta.
A date crossing MCP means what the caller sent. A DATE was rendered as an epoch instant in
UTC, so a row Oracle holds as 18:38 was reported as 17:38 — a wrong wall-clock time rather than a
differently-spelled one, on data an agent reads as a departure time. Dates now parse and render
against the server’s own clock, and a bare 1980-01-01 is accepted where it previously was not.
A config author can write the server’s instructions — the text a model reads before it reads any tool. It is prepended to the generated inventory rather than replacing it, and the box has room to type in.
The Design tabs commit a tick as you make it. The per-tab Save button is gone: selecting an object saves it. Settings also show their default, so an empty box is no longer ambiguous between “unset” and “set to the default”.
The audit settings are grouped, the two closed choices are dropdowns rather than free text, and a bad value gets an error page instead of a stack trace.
API documentation for com.mcpdbwizard.pub is published alongside this site at /javadoc/.
2.0.0 — 20 August 2026
The first release to carry a version number. Everything before it was built and run from source, so there is no upgrade path to describe and no earlier image to compare against.
What it stamped: the web console with its Design and Runtime halves, the /mcp/<config> proxy with
accounts and a per-config access matrix, generated MCP servers over Streamable HTTP, the audit
trail, Prometheus metrics, and pooled DAO factories.
Two things landed close enough to the stamp to be worth naming, because both changed how a record parameter behaves and both were failures you could not see:
- A record parameter now publishes its field names. It used to cross as a bare
{"type": "object"}, so a caller had no way to learn the keys. - An unrecognised key is now rejected. It used to be dropped, which bound the record all-null and ran the procedure anyway — a caller sending the Oracle column spelling got a constraint violation from deep inside PL/SQL, with nothing connecting it to the key they sent. The two shipped together on purpose: strictness before discoverability would have rejected keys nobody could have got right.
The field names are the generated Java ones rather than the Oracle column names, which is issue 3 on the known-issues page.