MCPDBWizard

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

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 namespayroll 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:

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

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.