Frequently asked
Questions we get asked
Short answers, with a pointer into the documentation where there is more. If something here contradicts the quickstart, the quickstart is right and this page is a bug.
How do I know it won't go rogue on me?
Because the thing that would have to go rogue was never written. The config you author decides what code is emitted: an Oracle object you did not select has no tool, no method and no class in the generated server. It is missing from the binary rather than blocked by a rule, so there is no prompt, no jailbreak and no clever argument that reaches it.
Four more things back that up:
- The caller never supplies SQL. The generator writes the table and document statements, a PL/SQL routine's body is your schema owner's, and any statement of your own is one you wrote and tested. An agent supplies bind values, never statement text.
- Arguments are checked before the database is touched.
Every tool has a strict JSON schema with
additionalProperties: false, so a misspelled or invented argument is refused by the server, not by Oracle. - The database grant is underneath all of it. Every caller shares one Oracle account. Give it least privilege and its grants cap everything above.
- Nothing runs unattended without a record. Every call is logged twice — see auditing and logging below.
The honest caveat: what you did select is exposed as written.
A selected delete tool deletes. Curation is the control, and
it happens before anything runs.
How do I connect it to Oracle?
Through the environment, not through a form. The Design pages have no connect dialog — host, port, SID, username and an optional second schema come from configuration, and everything except the password is shown in the UI so an operator can check what they are pointed at.
-e MCPDBWIZARD_ORACLE_HOST=db.example.com
-e MCPDBWIZARD_ORACLE_PORT=1521
-e MCPDBWIZARD_ORACLE_SID=/PDB1
-e MCPDBWIZARD_ORACLE_USER=appuser
-e DB_PASS_FILE=/run/secrets/oracle
A leading / on the SID selects the service-name form. The
password is the only Oracle secret, supplied as DB_PASS or —
better — as DB_PASS_FILE pointing at a Docker or Kubernetes
secret. A config saved from the UI never contains a password;
it records a FROM_ENV_VARIABLE_DB_PASS placeholder instead, so
there is nothing on the data volume to leak.
How can I spot MCPDBWizard code running inside Oracle?
Every connection this software opens names itself, by calling
DBMS_APPLICATION_INFO.SET_MODULE immediately after login. So
the answer is one query:
SELECT module, action, sid, serial#, status, sql_id
FROM v$session
WHERE module = 'MCPDBWizard' -- the generator, introspecting
OR module LIKE '%DaoFactory%'; -- a generated server
The generator reports module MCPDBWizard with action
Introspect. A generated server reports its own DAO
factory class name, so different configs are distinguishable from
one another rather than lumped together.
This matters more here than in an ordinary application: every caller is
the same Oracle user, so the username identifies nobody.
MODULE is the first column a DBA groups by when a session is
blocking, burning CPU or holding a lock, and it is what
DBMS_MONITOR and AWR key their per-module aggregates on.
You may also see helper object types in the schema — see CREATE TYPE below.
How does MCPDBWizard pay for itself?
Open core. The generator and the runtime library are Apache-2.0 and free, including for production use. The commercial half is the control plane around them — the Design and Runtime web application, the accounts and access matrix — together with support and version-currency assurance against the Oracle releases you actually run. Licensing is per production MCP server, annually, with non-production free. The commercials page has the detail.
Is it open source?
Partly, and the line is drawn deliberately.
-
app/— Apache License 2.0. The generator, the MCP server emission, and thecom.mcpdbwizard.pubruntime library that generated code links against. -
web/— proprietary, all rights reserved. The Design and Runtime web UI, the configuration store, curation and access control.
Code emitted by the generator is your own work and carries no licence obligation from this project. It links at run time against the Apache-2.0 runtime library, so generated applications may be distributed under any terms you choose.
The open module genuinely stands alone: it declares its own Maven coordinates, has no parent POM, and can be copied out of the repository on its own and built. A grant that could only be exercised with an all-rights-reserved file beside it would not be a grant.
How much of the code was generated by AI?
The generator's core is not AI-written: it is hand-written Java dating back to the early 2000s, and the accumulated Oracle data-dictionary knowledge in it is the product's actual asset.
Everything added from mid-2026 onwards — MCP server emission, the web control plane, 23ai and 26ai datatype support, OAuth, auditing, metrics — was written with AI assistance (Claude Code) under human review. That is most of the recent commit history.
The reason to be relaxed about it is what checks the work. Every change is regenerated and tested against a live estate of six Oracle instances spanning 12c, 18c, 19c, 21c, 23ai and 26ai, comparing generated output file by file. AI wrote a lot of it; a real database on six versions decides whether it was right.
How do I connect it to Grafana?
Through Prometheus. There are two endpoints, and confusing them is the usual reason a dashboard comes up empty.
- Container health — the web application on
:8080/metrics. One per container, always on, nothing to enable. Series aremcpdbwizard_container_*: CPU, memory and disk. - Per-tool call metrics — each generated server, on the
port you give it. Needs
PROMETHEUS_SERVER=YESin the config andMCP_METRICS_PORTat run time; neither half does anything alone. Series aremcpdbwizard_mcp_*.
scrape_configs:
- job_name: mcpdbwizard-container
static_configs:
- targets: ['mcpdbwizard-web:8080']
- job_name: mcpdbwizard-mcp
static_configs:
- targets: ['mcpdbwizard-web:9464']
You get call counts split by outcome, p50/p75/p90 latency, request and
response byte volumes, and the connection-pool numbers. Every series
carries a db_object label naming the Oracle object behind the
tool — DBHELL.OB_GEN_PKG.GREET rather than a flattened tool
name — so sum by (db_object) needs no join.
Two things to know before you alert on it. Set a container memory limit, or
the memory percentage is of the whole host and says so with a
basis="host" label. And a reading that cannot be taken is
absent from the scrape rather than zero, so alert on
absent(...) too.
How does auditing work?
Two records, from the two components that know different halves of the story. The proxy records who called which tool and whether it was allowed; each generated server records what the tool did, with which argument names, and how it ended. Correlate them by config, tool and time.
For a real trail, name a sink with MCP_AUDIT_SINK — both the
servers and the proxy send to it. A mistyped sink stops start-up rather
than leaving you silently unaudited. Kafka ships as the reference
implementation.
Set MCP_AUDIT_SPOOL_DIR as well. It turns on
a write-ahead spool: every record hits disk before any delivery
attempt and is removed only once the sink confirms it, so records survive a
sink outage and survive the server dying. Kafka alone is a delivery
mechanism; Kafka plus the spool is a trail. Delivery is at-least-once and
every record carries an id, so dedupe on it.
Argument values are not recorded by default. MCP_AUDIT_LEVEL=values turns them on, and you should switch it
on deliberately: at that level the record carries production data chosen by
a model, which makes your sink a store with retention, encryption and
erasure obligations.
How does logging work?
Both the web application and every generated server write to the
container's stdout, so docker logs and any standard log driver
or sidecar collect them. Lines are tagged [mcp:<config>]
and [gen:<config>] so you can tell which config produced
them, and the same output is kept per-runtime under the config directory,
which is what the Runtime page tails.
Every tool call produces one line:
MCP-CALL {"tool":"get_customer","outcome":"ok","ms":12,"args":["p_id"]}
Outcomes are ok, not-found,
document-changed, pool-exhausted,
database-error or error. Argument
names are recorded; values never are.
This is a diagnostic record, not an audit trail — the process being audited writes it, to a log it owns. For durability and tamper-evidence, configure a sink as above.
The backend is chosen at generation time but can be overridden at startup
with MCPDBWIZARD_LOG_BACKEND (console,
jul, text, slf4j,
log4j2), because the right backend depends on where the server
runs. A name that cannot be loaded stops the server rather than silently
falling back and leaving you believing you had redirected the logs.
How does security work?
It is layered, and the layers are deliberately of different kinds.
- Absence. Unselected objects have no code. This is the strongest layer because it is not enforced by anything at run time.
- The Oracle grant. One shared service account, least privilege, underneath everything.
- Accounts and the access matrix. The web application has many accounts, each admin or ordinary. A new account starts with no grants; the matrix is one row per account, one column per config, one tick per grant.
- Tokens at the proxy. Agents call
/mcp/<config>with an account API token, issued once and stored only as a BCrypt hash. No token gets 401, a valid token without a grant gets 403, and a config that is not granted is refused identically whether or not it exists — otherwise the proxy would be a way to enumerate config names. - The servers themselves bind loopback. Exposing one directly is supported but bypasses everything above, so the server refuses to start when exposed unless it was generated with bearer-token or OAuth authentication. TLS does not satisfy that guard: it encrypts the wire and restricts nobody.
- OAuth 2.1 for anything past a single trusted client —
audience-bound validation against your existing authorization server.
Plus
Originchecks against DNS rebinding, per-caller rate limits, an optional statement timeout and a request-body cap.
Where authorization lives is the design decision worth knowing. The matrix governs which configs an account may drive, not which tools within one. Tool-level curation stays in the config, where it is enforced by absence. Two teams needing different tools over the same schema is two configs — which is a stronger guarantee than a rule in a running process, not a weaker one.
How can I add my own SQL statements to the generated server?
On the SQL tab of the Design pages. Each statement you add becomes a tool
of its own, with its bind variables as typed, named arguments. The
statement text is stored in the config, so the config is
self-contained and versionable — there is no directory of loose
.sql files to keep in step with it.
These are statements you write, review and test, the same as any other application SQL. Give each one a clear description: that is what the model reads when it decides whether the tool applies.
Can I set it up so the agent can issue any SQL statement it wants?
No. That goes against the whole idea of the project.
There is no tool that takes SQL text as an argument, and no flag that adds one. If you want an agent authoring its own SQL against Oracle, several other projects will do that for you.
What is the backstory?
It began in Dublin in the early 2000s at Orinda Software Ltd, as a code generator that read an Oracle schema and emitted Java JDBC wrapper classes — DAO factories, callable-statement wrappers, table managers — so that applications could call PL/SQL without anyone hand-writing the binding for records, collections and ref cursors. That is a genuinely hard problem, and solving it took years of accumulated knowledge about how Oracle's data dictionary describes PL/SQL, including the places where it describes it wrongly.
MCP turned out to need exactly that work. An agent calling a PL/SQL procedure has the same problem an application had — the parameters have to cross a typed boundary correctly — so the generator gained a new emission target, and in August 2026 the product was renamed MCPDBWizard to say so. Ownership now rests with ATB Consultancy Services Ltd.
The practical consequence: this is not a wrapper written last year around
executeQuery. The awkward parts — a record whose fields are
collections, a strong ref cursor's row type, the dictionary quirks that
differ between 12c and 23ai — were solved a long time ago, against real
schemas.
What Oracle versions do you support?
The product was originally written for Oracle versions 8.1.5 through 12. The current supported minimum is 12, and we support the current release.
In practice that means every change is verified against 12c, 18c, 19c, 21c,
23ai and 26ai. The versions differ more than you would expect —
ALL_ARGUMENTS alone reports PL/SQL records differently on 12c
than on everything after it — which is why the estate exists rather than a
single test database.
Do you support databases other than Oracle?
An earlier incarnation of this project also supported DB2, and support could be re-activated if sufficient commercial interest was shown. Support for other products, such as Postgres and MySQL/MariaDB, would require a non-trivial R&D effort.
Why does my user need CREATE TYPE?
Only if you expose PL/SQL that takes collection or record
parameters — and then because JDBC cannot bind a PL/SQL collection
directly. The generator's answer is to create a matching pair of SQL object
types per record shape (<prefix>_T and
<prefix>_A) and bind through those. Their DDL is written
to extraObjects.sql beside the generated code, and the server
creates them itself at start-up when one of its tools needs them.
If granting CREATE TYPE is not acceptable, hand
extraObjects.sql to a DBA to run once as the schema owner. The
Runtime page tells you when the types are missing, and the server keeps
serving every tool that does not need them.
One related behaviour, because it fails loudly on purpose: these types are
created with CREATE OR REPLACE, which Oracle will not honour
once anything depends on them. If the underlying record has
changed, the old definition survives — and binding against it
would move values into the wrong columns silently. So the server stops,
names the type, and prints both definitions. It is the one place the
runtime fails closed over a data fault rather than a security one, and the
alternative is wrong answers with nothing to show for them.