Documentation
Get started
MCPDBWizard is a generator. You point it at an Oracle schema, select the objects you are willing to expose, and it emits Java for exactly those — then compiles it and runs it as an MCP server. An object you did not select has no tool, no method and no class, so the curation is enforced by absence from the binary rather than by a rule at request time.
One container holds both halves: the web application on 8080
(Design pages, Runtime page, user administration, and the MCP proxy) and
the servers it generates on 8090–8109, each its own JVM, bound
to loopback and reached through the proxy.
Requirements
- An Oracle database reachable from wherever the container runs. Oracle 12c through 26ai are supported.
- A dedicated database user for the agent. Every caller shares this one account — the server authenticates to Oracle, not its callers — so grant it the least privilege that works. That grant is your real backstop underneath the selection.
- An MCP client. Claude Desktop and Claude Code both work.
1. Run the container
The Oracle connection is configuration, not something anyone types into a form. Everything except the password is shown in the UI so an operator can check what they are pointed at.
docker run -d --name mcpdbwizard \
-p 8080:8080 \
-v mcpdbwizard-demo:/data \
-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 \
mcpdbwizard-web
A leading / on the SID selects the service-name form. Your
configs, accounts, access grants and each runtime's workspace live on the
/data volume, so replacing the image keeps them.
Open localhost:8080 and sign in as
admin / password. You are forced to replace that
before the generator will run.
The password is the one Oracle secret
Supply it as DB_PASS, or better as DB_PASS_FILE
pointing at a Docker or Kubernetes secret — that form stays out of
docker inspect, is not inherited by every child process, and
can be rotated by replacing the file. Set exactly one of the two; setting
both is an error rather than a precedence rule, because two sources for one
secret hides which credential is in use.
A config saved from the UI never contains a password. It
records a FROM_ENV_VARIABLE_DB_PASS placeholder where the
password would go, so there is nothing on the volume to leak.
2. Select the objects
On the Design pages, pick the tables, PL/SQL packages and routines and
sequences to expose, and add any SQL statements you have written and
tested yourself. Then save. That is the whole security model, and it is a
file you can review and put through a change process. Configs are plain
.pb2 properties or .json — the same content
either way:
# payroll.pb2
MCP_SERVER=YES
MCP_HTTP_TOKEN=YES
TABLE_USER_0=SYNUSER
TABLE_NAME_0=ACTIVITY_LOG
PROC_USER_0=SYNUSER
PROC_PACKAGE_0=JS_ADMIN
PROC_NAME_0=GETADMINTOOLINFO
SEQUENCE_USER_0=SYNUSER
SEQUENCE_NAME_0=JOB_ID
PASS=FROM_ENV_VARIABLE_DB_PASS Config names must be valid Java package names —
payroll or com.example.payroll, not
payroll-api. The name is a file, a directory, a path segment
in /mcp/<config> and the identity the access matrix
grants on, so it is limited to an alphabet all four agree about.
Writing good descriptions
Every tool gets a generated description carrying its columns or parameters with their Oracle types, and you can replace it with your own. The description is what the model reads when it decides whether to call a tool: say what the call does and when it applies, in plain language. Vague descriptions produce wrong tool choices far more often than bad parameters do.
Curation is per object, so use more than one config
A selected table yields at least four tools, so fifty tables is two hundred before any PL/SQL. If an agent is struggling to choose, that is a curation problem: make a second config selecting only what that agent needs. Configs are the unit of curation and of access, so this also narrows what the account can reach.
3. Generate and run it
On the Runtime page, pick the config and start it. The generator emits the Java, compiles it, and launches the server on the next free loopback port. Several configs run at once, each its own process.
Generated code is ordinary Java and does not need the web application — it can be run by hand, over HTTP or over stdio for a locally launched client.
4. Issue a token and grant the config
A browser signs in with a form; an MCP client cannot. On the Users page, issue the calling account an API token — shown exactly once, since only a BCrypt hash is stored — and tick that config on the Access grid. A token alone is not access: a new account starts with no grants.
5. Connect a client
Point the client at the proxy, not at the generated server. The proxy is the only component that knows who is calling; a generated server sees one shared Oracle account and a single bearer token, which is a door key rather than an identity.
{
"mcpServers": {
"payroll": {
"url": "http://localhost:8080/mcp/payroll",
"headers": {
"Authorization": "Bearer <id>.<secret>"
}
}
}
} Transport is Streamable HTTP. Responses come back as plain JSON or as an SSE frame, so a hand-rolled client must handle both. Ask the agent what it can do and it will list the tools back to you — a quick way to confirm the config generated as you intended.
What the tools are called
Lower-cased Oracle names joined by underscores, with the operation last:
<owner>_<table>_get_by_pk,_insert,_update,_delete<owner>_<table>_<constraint>— one lookup per selected unique key, index or foreign key<owner>_<package>_<routine>— overloads get a number appended, so two never collide<owner>_<sequence>_nextval<owner>_<view>_doc_get_alland friends, for 23ai JSON-relational duality views
How values cross
NUMBER is a JSON number; VARCHAR2,
CHAR and CLOB are strings; DATE and
TIMESTAMP are ISO-8601 strings; RAW,
BLOB and binary vectors are base64. On 23ai, native
JSON crosses as an object, BOOLEAN as a boolean
and a dense VECTOR as an array of numbers. A PL/SQL record or
SQL object type crosses as an object keyed by field, a collection as an
array, and an OUT ref cursor as an array of row objects.
A PL/SQL tool returns every OUT and IN OUT parameter as
one object keyed by parameter name, with a function's return value under
result. A routine with six OUT parameters gives you all six.
Types that cannot cross JSON honestly are not exposed:
SDO_GEOMETRY, BFILE, IN ref cursors and FLOAT16
vectors. A routine using one is skipped whole, and the generation log says
which and why.
Security worth knowing before you deploy
- Generated servers bind loopback. Exposing one directly bypasses the accounts, the grid and the per-caller limits. If you do it anyway, the server refuses to start unless it was generated with bearer tokens or OAuth — exposure is the one act that puts it on a network, while authentication is opt-in.
- OAuth 2.1 for anything past a single trusted client. Audience-bound token validation against your existing authorization server (Entra, Okta, Keycloak, Auth0). This server validates tokens; it does not issue them.
- TLS does not substitute for a token. It encrypts the wire and restricts nobody.
- The grid governs configs, not tools within one. Tool-level curation stays in the config, where it is enforced by absence from the binary. Two groups needing different tools is two configs.
Troubleshooting
The proxy returns 401
No token, or a stale or revoked one. Issue a fresh token on the Users page.
Note this is a different credential from the generated server's own
MCP_HTTP_TOKEN; the attempt is in the log as
"outcome":"bad-token".
403 on a config you can see in Design
The account is signed in but not granted that config. Tick it on the Access grid.
503, "is not running"
Nothing is serving that config. Start it on the Runtime page.
A call seems to do nothing
Tool schemas are strict — additionalProperties: false — so a
misspelled argument is rejected before the handler runs and the database is
never touched. The refusal still comes back as a result with content, so it
can look like a call that ran. Check the argument names against
tools/list.
"Server busy: all database connections are in use"
The connection pool is at its ceiling — load, not a fault. Raise
DAO_POOL_MAX_SIZE, or let clients retry.
Changing a setting did nothing
There are two kinds. Generation-time flags live in the config and decide what code is emitted — changing one means regenerating. Runtime environment variables are read when the server starts. Setting the environment variable alone does nothing if the server was generated without the matching flag: the code to read it was never written.
Further
The repository carries the two long-form guides this page summarises:
DEPLOYMENT.md for the operator's side — ports, credentials,
OAuth, TLS, pooling, rate limits, auditing, Prometheus metrics — and
USING-MCP.md for the caller's side, with every request and
response taken from a live server.