MCPDBWizard

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 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.

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.

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.

  1. Absence. Unselected objects have no code. This is the strongest layer because it is not enforced by anything at run time.
  2. The Oracle grant. One shared service account, least privilege, underneath everything.
  3. 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.
  4. 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.
  5. 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.
  6. OAuth 2.1 for anything past a single trusted client — audience-bound validation against your existing authorization server. Plus Origin checks 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.