<p align="center"> <img src=".github/assets/hero.svg" alt="oraclemcp: governed, least-privilege Oracle Database MCP server in pure Rust" width="100%"> </p>
<p align="center"> <a href="https://github.com/MuhDur/oraclemcp/actions/workflows/ci.yml"><img src="https://github.com/MuhDur/oraclemcp/actions/workflows/ci.yml/badge.svg" alt="CI"></a> <a href="https://crates.io/crates/oraclemcp"><img src="https://img.shields.io/crates/v/oraclemcp.svg" alt="crates.io"></a> <a href="#license"><img src="https://img.shields.io/badge/license-Apache--2.0%20OR%20MIT-blue.svg" alt="license"></a> <img src="https://img.shields.io/badge/unsafe-forbidden-success.svg" alt="forbid(unsafe_code)"> <img src="https://img.shields.io/badge/rustc-nightly--2026--05--11-orange.svg" alt="nightly-2026-05-11"> </p>
Governed, least-privilege Oracle Database access for AI agents — in pure Rust.
oraclemcp is a Model Context Protocol server that gives an AI agent governed, least-privilege access to an Oracle database: schema introspection, DDL, compile errors, source search, ad-hoc read queries, plan analysis, and an explicit profile-gated execution path for non-read SQL. Every raw statement the agent submits is classified before it can reach Oracle. Read tools only admit statements proven read-only; oracle_execute only runs statements permitted by the active profile/session level, rolls DML back by default, and requires a preview-derived execution grant before commit. Session elevation is explicit, temporary, and capped by profile max_level. The core is engine-free and #![forbid(unsafe_code)].
_An independent open-source project; not affiliated with Oracle. For Oracle's own MCP servers, see oracle/mcp._
Install, service, dashboard
One line installs or updates oraclemcp on macOS and Linux. It works as pasted for a human terminal and for a non-interactive agent run:
curl -fsSL "https://raw.githubusercontent.com/MuhDur/oraclemcp/main/install.sh?$(date +%s)" | bash
The hosted script fetch includes a cache buster so stale CDN/proxy copies do not hide installer updates. This command installs the latest published release; add --version X.Y.Z (or vX.Y.Z) to pin a specific release instead. Later examples that contain ..., <pw>, <profile>, or placeholder env values are templates: replace those placeholders before running them.
The normal command downloads, verifies, and installs into $HOME/.local unless you pass --prefix. It requires the SHA-256 digest check, verifies the cosign blob signature and provenance attestation when cosign is installed, and installs oraclemcp plus the short om alias. Missing cosign is a visible authenticity-unverified posture by default; use --verify require when your environment requires cosign to be present.
In an interactive terminal, the installer then offers a short guided flow: append the binary directory to PATH, run doctor, offer zero-config database discovery from tnsnames.ora, print an MCP client snippet, and optionally install the loopback service. In a pipe, CI job, or agent run, it never prompts, never scans, and never starts a service; it installs the binary and prints the exact PATH line plus next steps on stderr. Every install finishes with next steps on stderr: discover databases, run doctor, write the starter profile, and generate MCP client snippets.
Zero-config onboarding
oraclemcp setup --discover finds every database defined in your tnsnames.ora and writes one read-only connection profile per net-service — through the same governed config-ops path (timestamped backup, atomic write, strict re-validation) used everywhere else. It is consent-gated: an interactive run asks before it scans and again before it writes; a non-interactive run without --discover-tns (or --yes) refuses with exit code 2 and scans nothing. It writes no secrets to disk (each profile references an environment variable, env:ORACLE_<NAME>_PASSWORD, that you export yourself), keeps every profile capped at READ_ONLY, and is idempotent and non-destructive: existing profiles and hand edits are preserved, only new databases are added. When no tnsnames.ora is found it falls back to the minimal starter profile so you still boot. Add --json for a names-only agent report, or --dry-run to preview without writing. Run oraclemcp doctor afterwards to see exactly which credentials remain to be set. Full contract: docs/tns-discovery-onboarding.md.
Re-running the same one-liner is the update path. Re-running the same verified archive is a no-op for identical installed files; re-running with a newer target updates atomically after backing up the previous binary. A downgrade is refused unless you pass --force.
Operator migration notes for the current field-hardening train: docs/oraclemcp-091-field-hardening-notes.md. Config-migration runbooks introduced in 0.8.0 still apply when upgrading from an older release: docs/upgrading-to-0.8.0.md, docs/downgrading-0.8.0-to-0.7.2.md, and docs/feature-rollout-0.8.0.md.
Use the dry-run command first when you want a preview: it prints the archive, verification inputs, files, service plan, client-registration plan, and installer lock path, then exits before downloading, verifying, writing files, or touching the service manager. Dry-run exists for review and automation plans; the normal command above is the install/update command.
Advanced install paths
Preview the Linux/macOS host plan without changing the machine:
curl -fsSL "https://raw.githubusercontent.com/MuhDur/oraclemcp/main/install.sh?$(date +%s)" | bash -s -- --dry-run
From an installed binary, preview or run the same update path:
oraclemcp --json self-update --dry-run
oraclemcp self-update --no-service
On Windows, download and run the PowerShell installer:
iwr -UseBasicParsing https://raw.githubusercontent.com/MuhDur/oraclemcp/main/install.ps1 -OutFile install.ps1
powershell -ExecutionPolicy Bypass -File .\install.ps1 -DryRun
powershell -ExecutionPolicy Bypass -File .\install.ps1
The Windows installer accepts the same release operations: -Update for the explicit update path, -NoService to suppress service prompts, and -Verify prefer, -Verify require, or -Verify checksum-only for the verification posture. prefer installs after a hard SHA-256 check when cosign is missing; require fails without cosign.
powershell -ExecutionPolicy Bypass -File .\install.ps1 -Update -NoService
For air-gapped hosts, stage five inputs: the release archive, its .sha256, .sigstore.json, and .attestation.sigstore.json siblings, plus a Sigstore trusted_root.json obtained independently on a connected staging host. With a trusted Cosign v3 installation, refresh that root through Sigstore's TUF metadata before moving it across the air gap:
cosign trusted-root create --with-default-services --out sigstore-trusted-root.json
Then require authenticity and provenance verification during the offline install:
bash install.sh \
--offline ./oraclemcp-x86_64-unknown-linux-musl.tar.gz \
--version 0.10.0 \
--verify require \
--trusted-root ./sigstore-trusted-root.json
powershell -ExecutionPolicy Bypass -File .\install.ps1 `
-Offline .\oraclemcp-x86_64-pc-windows-msvc.zip `
-Version 0.10.0 `
-Verify require `
-TrustedRoot .\sigstore-trusted-root.json
The trusted root is trust material, not another self-authenticating release asset. Provision and protect it separately from the archive bundle. Offline Cosign verification fails closed when it is absent; checksum-only remains an explicit integrity-only posture.
The release installer does not silently fall back from a missing release archive to a source build. Use --source explicitly when you want cargo install instead of the verified archive path.
On Linux the installer auto-detects the static musl build, which runs everywhere (including WSL2). The published glibc tarballs are also installable, but only by explicit request: --target x86_64-unknown-linux-gnu (or aarch64-unknown-linux-gnu).
Uninstall is preview-first and idempotent. Service removal remains an explicit service-manager mutation:
bash install.sh --uninstall --dry-run
bash install.sh --uninstall --yes
bash install.sh --uninstall --service --yes
powershell -ExecutionPolicy Bypass -File .\install.ps1 -Uninstall -DryRun
powershell -ExecutionPolicy Bypass -File .\install.ps1 -Uninstall -Yes
Install the local service only with explicit consent. Keep it on loopback unless you deliberately configure remote HTTP, and use service-owned client credentials, OAuth, or mTLS for HTTP MCP clients. For Windows service install, the PowerShell installer also requires explicit consent.
oraclemcp --json service install --dry-run --profile db_ro --listen 127.0.0.1:7070 --client-credentials
oraclemcp service install --yes --profile db_ro --listen 127.0.0.1:7070 --client-credentials
oraclemcp --json clients issue --label claude --scope oracle:read
powershell -ExecutionPolicy Bypass -File .\install.ps1 -Service -Yes -Profile db_ro
Request a listener-bound pairing URL plus a one-time code, open the printed URL, and paste the code into the form it serves:
om dashboard
The printed URL carries no secret, so it is safe in browser history, in a Referer, and in the view of any extension holding tabs/webNavigation permission. The one-time code is accepted only from the pairing form's POST body — never from a URL query or fragment — and the CLI never hands either to a desktop launcher (where process argv could expose it). The dashboard uses a one-time loopback pairing ticket bound to the exact live listener instance and scheme/host/port, then an HttpOnly, SameSite=Strict cookie plus CSRF and route-scoped action tickets. The cookie is Secure under native TLS or explicit trusted HTTPS termination; the only non-Secure exception is server-observed loopback HTTP, and remote plaintext requests never receive privileged browser cookies. Browser requests do not supply the database Subject: the server derives the Subject from the authenticated transport principal, session, and lane context. Authenticated HTTP sessions run on isolated per-principal lanes with their own Oracle connection, operating level, grants, cancellation, and audit context. Intentional --allow-no-auth HTTP development uses one anonymous lane; stdio remains the single local client path.
Other release channels come from the same signed archive matrix. These channels can lag the GitHub release tag, so use the check command first and install only after it resolves the target version.
cargo binstall oraclemcp
docker run -i --rm ghcr.io/muhdur/oraclemcp:latest
Pending registry-backed channels:
brew info MuhDur/oraclemcp/oraclemcp
winget search --id MuhDur.oraclemcp --exact
After the relevant check resolves the target version, these commands are copy-pasteable:
brew install MuhDur/oraclemcp/oraclemcp
winget install --id MuhDur.oraclemcp --exact
An npm/npx channel is not offered. Install with the one-line installer above, or cargo binstall oraclemcp, the GHCR Docker image, or the Homebrew/winget channels once they resolve.
Why oraclemcp
- Fail-closed by construction. A SELECT that an agent dreams up should never silently turn into a
DELETE. Each raw statement runs through the hardened classifier. Read tools admit only proven read-onlySELECT/WITHand dictionary introspection. Non-read execution is isolated inoracle_execute, bounded by profilemax_level/default_level, rollback-by-default for DML, and explicit-confirm-before-commit. Temporary elevation throughoracle_set_session_levelcan never exceed the profile ceiling. Forbidden constructs (multi-statement batches, string-concat dynamic SQL, an unproven function call inside a SELECT) are rejected before touching the database, with anOperatingLevelTooLoworForbiddenStatementenvelope and a suggested safe alternative. - Agent-first UX. Every tool ships a real JSON Schema, title, and explicit MCP annotations (
readOnlyHint,destructiveHint,idempotentHint,openWorldHint) so clients do not infer unsafe defaults. Errors are structuredErrorEnvelopes with machine-stable classes, fuzzy suggestions, and next-step hints, not bare strings. A zero-argoracle_capabilitiestool lets an agent discover the surface; MCP resources expose the capability/tool documents plus schema/object read templates; and an offline build degrades to aRuntimeStateRequiredcontract instead of crashing. - Pure Rust, no
unsafe. Every crate is#![forbid(unsafe_code)]; the fail-closed classifier carries a differential cargo-fuzz target. - Two transports. stdio (default) and Streamable HTTP (
--listen) with
fail-closed auth defaults, optional OAuth bearer enforcement, and native rustls TLS/mTLS.
Source builds and runtime requirements
This branch is pinned to nightly-2026-05-11 and has no stable MSRV. The pin is required, for two independent reasons: this checkout resolves asupersync 0.3.9, whose nightly-outcome-try feature enables #![feature(try_trait_v2)] and try_trait_v2_residual inside asupersync (it is opt-in, but in asupersync's default feature set, and reaches us through the oraclemcp-driver-cx dependency), and on Windows oraclemcp-core additionally needs windows_by_handle. The pinned oraclemcp-driver-cx 0.9.2 driver's own source is stable-clean — it is its asupersync dependency declaration that pulls the nightly feature in. docs/toolchain.md has the exact mechanism. The repository's rust-toolchain.toml selects the pin for local builds. Use the release installer above when you want the prebuilt binary; use cargo install only when you intentionally want a source build.
rustup toolchain install nightly-2026-05-11 --component rustfmt --component clippy
Direct source install:
cargo +nightly-2026-05-11 install oraclemcp
Live database access is built in through the pure-Rust thin oraclemcp-driver-cx driver.
Runtime requirements for live database access:
- Optionally
TNS_ADMINpointing at a directory withtnsnames.oraif you connect by net-service name.
No Oracle Instant Client, ODPI-C library, or C toolchain is required by the driver.
Use oraclemcp --json doctor to verify the binary and offline setup, oraclemcp --json doctor --profile <profile> to inspect non-secret profile metadata without resolving secrets, and oraclemcp --json doctor --online --profile <profile> to add live connectivity, authentication, role/open-mode, standby, and privilege checks. oraclemcp doctor oauth --token <JWT> validates one supplied OAuth token against the local resource-server config before any live connection; the token is never logged, persisted, or rendered. Doctor output is safe to paste into agent sessions: it omits connect strings, usernames, credential_ref values, passwords, proxy identities, wallet passwords, IAM tokens, wallet paths, and server DNs while keeping structured failure classes and ORA codes visible.
Generate generic local setup templates for profiles, wrappers, and MCP client snippets:
oraclemcp --json setup --profile db_ro
To create a minimal starter profiles file directly, use the same config-ops backend the dashboard uses. This validates the draft, writes a backup, atomically replaces the target, and reports the reload/rollback metadata without echoing the raw profile TOML:
oraclemcp --json setup --write --profile db_ro
Docker: a ready-to-run thin-driver image, published to GHCR and listed in the MCP registry on release as io.github.MuhDur/oraclemcp. The image defaults to UID/GID 10001:10001. On Linux, run it as your invoking non-root UID/GID so a private host config remains readable and its private state mount remains writable:
if [ "$(id -u)" -eq 0 ]; then
printf '%s\n' 'Refusing to run oraclemcp as root.' >&2
else
container_state="${XDG_STATE_HOME:-$HOME/.local/state}/oraclemcp-container"
mkdir -p "$container_state" && chmod 0700 "$container_state" &&
docker run -i --rm --user "$(id -u):$(id -g)" \
-v "$HOME/.config/oraclemcp:/home/oraclemcp/.config/oraclemcp:ro" \
-v "$container_state:/home/oraclemcp/.local/state/oraclemcp" \
-e ORACLE_APP_PASSWORD \
ghcr.io/muhdur/oraclemcp:latest
fi
docker run -i --rm ghcr.io/muhdur/oraclemcp:latest # tool surface only (no DB)
An optional PL/SQL intelligence image is available from the manual Docker workflow. It is the same server compiled with --features plsql-intelligence; it can start without a database connection and advertises the offline oracle_plsql_* tools immediately. Live PL/SQL tools still require a profile.
docker run -i --rm ghcr.io/muhdur/oraclemcp:plsql-intelligence-latest --json info
docker run -i --rm ghcr.io/muhdur/oraclemcp:plsql-intelligence-latest capabilities
Local feature-image builds resolve the PL/SQL engine crates from crates.io:
docker buildx build \
--target runtime-plsql-intelligence \
-t oraclemcp:plsql-intelligence .
docker run -i --rm oraclemcp:plsql-intelligence --json info
The Docker image and crates are Apache-2.0 OR MIT and do not redistribute Oracle Instant Client.
Wire it into an MCP client (e.g. Claude Desktop) over stdio:
{
"mcpServers": {
"oracle": {
"command": "oraclemcp",
"args": ["serve", "--profile", "db_ro", "--allow-no-auth"]
}
}
}
For Codex-style TOML config, the same command is:
[mcp_servers.oracle]
command = "oraclemcp"
args = ["serve", "--profile", "db_ro", "--allow-no-auth"]
Or run it directly:
oraclemcp serve # stdio (default); --allow-no-auth for local dev
oraclemcp --json clients issue --label claude --scope oracle:read # shown-once HTTP bearer
oraclemcp serve --listen 127.0.0.1:7070 --client-credentials --profile db_ro
claude mcp add oracle --transport http http://127.0.0.1:7070/mcp --header "Authorization: Bearer <bearer>"
oraclemcp serve --listen 127.0.0.1:7070 --allow-no-auth # local HTTP dev only
oraclemcp --json setup --profile db_ro # generic onboarding templates
oraclemcp --json setup --write --profile db_ro # write starter profiles via SCFG
oraclemcp capabilities # the advertised tool surface + feature tiers (JSON)
oraclemcp --json profiles # configured profile names and non-secret metadata
oraclemcp doctor # offline diagnostics (thin driver, TNS/wallet, classifier, NLS)
oraclemcp doctor --profile dev_ro # inspect profile metadata offline
oraclemcp doctor --online --profile dev_ro # include live connectivity/auth/role/privilege checks
oraclemcp info # build info: version, tools, transports, thin DB
oraclemcp robot-docs guide # compact in-binary guide for agents
oraclemcp completions bash # shell completions: bash, zsh, fish, powershell
oraclemcp --json service install --dry-run --profile db_ro # preview systemd/launchd/Windows service changes
oraclemcp service install --yes --client-credentials --profile db_ro
oraclemcp --json service status # inspect service-manager state
oraclemcp --json service logs # inspect recent service logs
oraclemcp --json service backup --dry-run # preview state+config backup
oraclemcp --json service restore /path/to/backup --dry-run # verify audit chain before restore
oraclemcp dashboard # open the local dashboard through a one-time pairing URL
--json is a visible alias for --robot-json and keeps stdout as a single machine-readable JSON object.
Stdio init-token clients
ORACLEMCP_STDIO_TOKEN (or serve --stdio-token) enables a handshake token for a custom MCP client that controls the raw initialize request. The client must send the shared token as a JSON string at exactly params._meta["oraclemcp/initToken"]; a missing key or a non-string value is reported as missing. This is not a generic Claude/Codex-style configuration snippet: mainstream MCP client configuration surfaces do not provide a way to inject initialize metadata. If the client cannot control that frame, keep stdio local and use --allow-no-auth deliberately, or use authenticated HTTP instead.
Release archives also include om (om.exe on Windows) as an argv0-aware short alias. om dashboard is equivalent to oraclemcp dashboard and uses the short name in CLI help and dashboard diagnostics when invoked through that alias.
oraclemcp service install targets the platform user service manager: systemd --user on Linux, launchd on macOS, and Windows services on Windows. Mutating service operations (install, uninstall, restart, backup, restore) require --yes; --dry-run emits the exact file and command plan without changing the host. Generated service definitions include bounded host caps for the 64-lane default: systemd uses Type=notify, NotifyAccess=main, Restart=on-failure, LimitNOFILE=65536, TasksMax=512, MemoryMax=2G, and OOMScoreAdjust=100; launchd uses KeepAlive plus file/process SoftResourceLimits; Windows configures automatic start and restart-on-failure through sc.exe. oraclemcp --json doctor reports those configured caps plus the effective open-file, task, memory-cgroup, and OOM caps visible to the current process. service backup snapshots the XDG service state directory plus the resolved profiles config into a new manifest directory; service restore verifies the backed-up audit hash-chain before stopping the service, restoring files, and starting it again. Streamable HTTP auth rules are unchanged for service mode: configure service-owned per-client credentials, OAuth, or mTLS with registered client leaf fingerprints, or pass --allow-no-auth only for intentional local development. The HTTP service also owns a private service-instance.json lock in its state root ($XDG_STATE_HOME/oraclemcp, or $HOME/.local/state/oraclemcp when XDG is unset). A second serve --listen process using that same state root refuses to start and reports the existing pid/listen metadata instead of silently taking over another port or socket. For intentionally independent instances, give each process a distinct XDG_STATE_HOME and listener port; that also separates their service-owned credentials, audit records, and durable state.
The browser dashboard is paired separately even on loopback. oraclemcp dashboard creates a 0600 one-time ticket under the user runtime directory and prints a secret-free /dashboard/pair URL alongside a one-time code. That URL serves a script-free form; submitting the code POSTs it in the request body, and the server exchanges it for an HttpOnly, SameSite=Strict dashboard cookie. The code is never read from the request target — a /dashboard/pair?ticket=... URL is refused outright without consuming the ticket — so the bootstrap secret cannot be recovered from browser history, a Referer, an access log, or a browser extension watching navigations. The cookie is Secure under native TLS or [http].trusted_https_termination = true; non-Secure cookies are limited to server-observed loopback HTTP. Forwarded scheme headers are ignored, and remote plaintext requests do not receive privileged browser cookies. The ticket expires in 60 seconds and is single-use; dashboard POSTs also require same-origin headers, a CSRF token, and a route-scoped action ticket. The dashboard origin threat-model addendum documents why the browser contract stays fail-closed: docs/dashboard-origin-threat-model-addendum.md.
The dashboard Workbench is not a terminal or SQL shell. It is disabled unless [http].dashboard_workbench = true; the Database Explorer remains available for governed metadata browsing when it is off. Classify and preview actions forward to oracle_preview_sql, read execution forwards to oracle_query, and guarded DML forwards to oracle_execute with the same single-use confirmation grant and audit path agents use. Browser-originated DDL/Admin apply remains blocked; DDL can be previewed, but applying it requires a non-browser operator path. When compiled with plsql-intelligence, the Workbench IDE panel also exposes the static oracle_plsql_parse, oracle_plsql_analyze, oracle_plsql_lineage, oracle_plsql_sast, oracle_plsql_doc, and oracle_plsql_what_breaks tools for source navigation, dependency, lint, doc, and impact previews; live snapshot/blast-radius tools remain outside the browser allowlist. The Reviews board stores profile-scoped Change Proposals as service-owned SQL templates plus captured binds, then applies them by re-classifying each template and forwarding through the same guarded action route; stored proposal verdicts are never authorization inputs. For source-replaceable CREATE OR REPLACE DDL, proposal apply captures the prior source into content-addressed service files before dispatch when the current source is visible. /operator/v1/source-history lists source-free snapshot metadata, and dashboard revert creates a normal DDL Change Proposal from the stored snapshot instead of bypassing review, confirmation, or profile ceilings. The Explorer page includes global search across visible schemas: object-name matches use oracle_search_objects with all object types, and source-text matches use oracle_search_source; both are sent through the same guarded operator action route as the rest of the dashboard. The Reviews page can also compare two supplied schema snapshots and export a reviewable migration script. The diff view omits raw DDL and shows hashes/counts; any executable export step must be drafted into the normal Change Proposal board before apply, where the server re-classifies and re-checks the statement.
The Streamable HTTP transport (--listen) fails closed. It starts only when service-owned per-client credentials, OAuth bearer enforcement, mTLS client-certificate verification, or --allow-no-auth is supplied, and mTLS requests become application principals only through registered leaf fingerprints. It refuses any non-loopback bind unless ORACLEMCP_HTTP_ALLOW_REMOTE=1 is set. Per-client credentials are one bearer per MCP client; the bearer is shown once by oraclemcp clients issue or oraclemcp clients rotate, while clients.json stores only salted hashes:
oraclemcp --json clients issue --label claude --scope oracle:read
oraclemcp serve --listen 127.0.0.1:7070 --client-credentials --profile db_ro
claude mcp add oracle --transport http http://127.0.0.1:7070/mcp --header "Authorization: Bearer <bearer>"
oraclemcp --json clients rotate <client_id>
oraclemcp --json clients revoke <client_id>
Online client-credential lifecycle
For a running HTTP service, use the dashboard's Client Credentials control or the authorized operator routes: GET /operator/v1/client-credentials, POST /operator/v1/client-credentials/rotate, and POST /operator/v1/client-credentials/revoke. The two mutation routes accept {"client_id":"..."}. Browser calls use the normal dashboard pairing, CSRF, and route-action tickets; non-browser calls need the normal /operator/v1 authority. Do not expose these controls to ordinary MCP clients.
A rotate or revoke is a per-client online lifecycle operation, not a service restart procedure. After its credential-store mutation is authoritative, the server removes that client's HTTP sessions and buffered SSE results, closes its stateful lanes, and installs the new credential generation as the admission floor. A request that authenticated with the old bearer but is still creating a lane is refused rather than gaining a fresh lane; subsequent requests with the old bearer fail authentication. On rotation, retain the one-time replacement bearer and reconnect with it to establish a fresh session. Other clients keep their sessions and bearers. Database credentials and OAuth/audit signing keys have separate restart lifecycles; see the operations guide.
OAuth configuration can come from profiles.toml or CLI flags. The resolved HS256 secret must be at least 32 bytes (256 bits); use randomly generated key material rather than a password or memorable phrase:
export ORACLEMCP_OAUTH_HS256_SECRET='replace-with-a-long-random-secret'
oraclemcp serve --listen 127.0.0.1:7070 \
--oauth-resource http://127.0.0.1:7070/mcp \
--oauth-issuer https://issuer.example.com \
--oauth-authorization-server https://issuer.example.com \
--oauth-required-scope oracle:read \
--oauth-hs256-secret-ref env:ORACLEMCP_OAUTH_HS256_SECRET \
--http-allowed-host 127.0.0.1:7070 \
--http-allowed-origin https://client.example.com
When OAuth is enabled, /.well-known/oauth-protected-resource stays public, /mcp requires a valid bearer token, and granted oracle:* scopes lower the request's effective operating ceiling monotonically. oracle:read caps the request at READ_ONLY, oracle:write/oracle:execute at READ_WRITE, oracle:ddl at DDL, and oracle:admin at ADMIN; none of them can raise a profile above its max_level, and protected profiles remain READ_ONLY. JWT bearer tokens must use the RFC 9068 access-token profile: the protected typ header is at+jwt (or application/at+jwt), and iss, sub, aud, exp, client_id, iat, and jti have their required access-token shapes. Generic JWTs and OpenID Connect ID tokens are rejected; there is no implicit generic-JWT compatibility mode.
OAuth bearer-token contract
--oauth-hs256-secret-ref is a secret reference, not an encoded-key setting. The server resolves the reference and uses the resolved value's raw UTF-8 bytes as the HS256 key; it does not base64- or hex-decode them. For example, env:ORACLEMCP_OAUTH_HS256_SECRET reads that environment variable, while putting a raw key in the field makes the literal env:... text the key. The resolved key must contain at least 32 bytes.
An accepted access token has typ: at+jwt (or application/at+jwt), a supported alg, and non-empty string iss, sub, client_id, and jti claims; numeric iat; a future numeric exp; and an aud that is either the configured resource string or an array containing it. The issuer must exactly match http.oauth.allowed_issuers. A token supplies scopes as either a space-delimited scope string or an scp array, and must satisfy every non-empty http.oauth.required_scopes entry.
Rejected requests keep the response body and WWW-Authenticate challenge generic: a presented but rejected bearer always receives error="invalid_token" with no error_description. This prevents an unauthenticated caller learning whether a token was malformed, signed correctly but expired, or missing a particular claim. The server records the fixed rejection category in its operator audit/security trail; it never records or echoes a bearer token, signature, or untrusted issuer value.
Native TLS uses rustls when [http.tls] or --tls-cert / --tls-key are configured. Adding [http.tls.client_ca_path] or --mtls-client-ca requires client certificates (mTLS) verified against that CA, but a CA-verified cert is not an application identity until its leaf DER SHA-256 fingerprint is listed in [http.mtls].client_fingerprints or passed with --mtls-client-fingerprint. The resulting principal key is mtls:sha256:<hex>. Server-only TLS encrypts the transport but is not application authentication, so /mcp still needs per-client credentials, OAuth, or an explicit --allow-no-auth development opt-in. Non-loopback binds require ORACLEMCP_HTTP_ALLOW_REMOTE=1 even with TLS. Native TLS on a non-loopback listener emits Strict-Transport-Security: max-age=31536000; includeSubDomains; loopback HTTPS deliberately omits HSTS so browser pinning cannot disrupt local HTTP development.
When Claude Code connects over HTTPS to a self-signed or private-CA listener, start it with that CA PEM in Node's trust store: NODE_EXTRA_CA_CERTS=/path/to/private-ca.pem claude.
Connection profiles are resolved from layered configuration (oraclemcp-config); select one with serve --profile <name>.
Connection profiles
See also:
oraclemcp.example.tomlis a fully annotated, copy-pasteable config showing every field with its default;docs/configuration.mdis the canonical field reference (types, defaults, precedence, the operating-level ladder, themcp_exposedopt-out, auth modes, andbaseinheritance). Theoraclemcp setup --writestarter is intentionally smaller so it can boot before you add wallet, proxy, DRCP, pool, app-context, or writable-profile settings.
Signed audit and unsigned refusal trail
For live database access, create ~/.config/oraclemcp/profiles.toml:
schema_version = 2
default_profile = "dev_ro"
# Optional least-privilege profile for fleet-wide DB observability.
# monitor_profile = "monitor_ro"
[http]
allowed_hosts = ["127.0.0.1:7070"]
allowed_origins = ["https://client.example.com"]
json_response = true
stateful = false
dashboard_workbench = false
[http.oauth]
resource = "http://127.0.0.1:7070/mcp"
allowed_issuers = ["https://issuer.example.com"]
authorization_servers = ["https://issuer.example.com"]
required_scopes = ["oracle:read"]
hs256_secret_ref = "env:ORACLEMCP_OAUTH_HS256_SECRET"
# Optional native HTTPS / mTLS listener.
# [http.tls]
# cert_chain_path = "/path/to/server-chain.pem"
# private_key_path = "/path/to/server-key.pem"
# client_ca_path = "/path/to/client-ca.pem" # require mTLS client certs
#
# [http.mtls]
# client_fingerprints = ["sha256:<client-leaf-der-sha256>"]
#
# Optional dedicated remote incident-response ingress. This is a second,
# separately bounded listener; it is mandatory-mTLS and accepts only registered
# certificates. The same fingerprint must also be allow-listed as an operator.
# [http.control]
# listen = "0.0.0.0:7071"
# preauth_workers = 4
# operator_workers = 1
# doctor_workers = 1
# The authenticated control probe bounds each request header and body to one
# second to limit slowloris exposure. A slow request is closed and emits a
# warn-level `reason=ingress_timeout` event; an unregistered certificate emits
# a separate warn with its computed `mtls:sha256:...` fingerprint.
#
# [http.operator]
# allow_loopback_owner = true
# allowed_subjects = ["mtls:sha256:<client-leaf-der-sha256>"]
# Signed audit chain. It records every privileged action in an append-only,
# hash-chained, HMAC-SHA256-signed JSONL stream. Omit `path` to use
# $XDG_STATE_HOME/oraclemcp/audit/audit.jsonl (or
# $HOME/.local/state/oraclemcp/audit/audit.jsonl when XDG_STATE_HOME is unset).
[audit]
path = "/var/lib/oraclemcp/audit/audit.jsonl"
# `key_ref` resolves through SecretResolver (env:, file:, or keyring:); its
# resolved value must be at least 32 bytes of independently random material.
key_ref = "env:ORACLEMCP_AUDIT_KEY"
key_id = "2026-q3"
# Retain old verification-only keys during rotation.
# [[audit.verification_keys]]
# key_id = "2026-q2"
# key_ref = "env:ORACLEMCP_AUDIT_KEY_2026_Q2"
# When no signed auditor exists because every reachable profile is READ_ONLY,
# this separate redacted refusal/security-event floor is on by default at
# $XDG_STATE_HOME/oraclemcp/corpus/refusals.jsonl. It is unsigned and not
# tamper-evident; it never replaces the signed chain. Set false only to opt out.
unsigned_refusal_log = true
[[profiles]]
name = "dev_ro"
description = "Read-only development database"
connect_string = "localhost:1521/FREEPDB1"
username = "APP_READONLY"
credential_ref = "env:ORACLE_APP_PASSWORD"
max_level = "READ_ONLY"
default_level = "READ_ONLY"
require_signed_tools = true
dashboard_ddl_workbench = false
# Optional Oracle call timeout and request-budget ceiling. Omit for the 30s
# default; set 0 only to opt out deliberately. Tool calls can tighten it with
# timeout_seconds where advertised.
call_timeout_seconds = 30
# Optional thin Session Data Unit request. Validated as 512..=65535 bytes.
sdu = 32768
login_statements = [
"ALTER SESSION SET NLS_LANGUAGE = english",
"ALTER SESSION SET PLSQL_WARNINGS = 'ENABLE:ALL'",
]
# Optional trusted local setup, authored by the profile owner and never by the
# agent. Use for session-local initialization that is not an ALTER SESSION.
trusted_session_statements = [
"BEGIN DBMS_OUTPUT.ENABLE(500000); END;",
]
[profiles.oci]
# Optional TCPS/wallet fields. Prefer these named fields over raw
# connect_string query parameters when the value should be validated or redacted.
wallet_location = "/etc/oracle/wallet"
wallet_password_ref = "env:WALLET_PASSWORD"
ssl_server_dn_match = true
ssl_server_cert_dn = "CN=dbhost.example.com"
# Optional SNI override. Omit for the driver default; set true only when the
# endpoint's routing name is a rustls-valid DNS name.
# use_sni = true
# Optional proxy authentication. If enabled, `credential_ref` belongs to
# `proxy_user`; omit top-level `username` or set it to the same value.
# The database needs: ALTER USER <target_schema> GRANT CONNECT THROUGH <proxy_user>
# [profiles.proxy_auth]
# proxy_user = "MCP_PROXY"
# target_schema = "APP_OWNER"
# Optional DRCP server routing. Prefer these named fields over raw
# connect_string query parameters so inheritance, validation, and redaction stay
# predictable. This is separate from [profiles.pool], which controls local
# client-side reuse.
[profiles.drcp]
pooled = true
connection_class = "ORACLE_MCP_AGENTS"
purity = "reuse"
# Optional local client-side pool for stateless metadata/catalog reads where
# pool-backed reads are used.
# User SQL, LOB/sample reads, DBMS_OUTPUT, transactions, and session state stay
# on the pinned main session. Served stateless HTTP uses bounded read-worker
# lanes instead of sharing one pool across lane runtimes.
# [profiles.pool]
# max_size = 4
# min_idle = 1
# acquire_timeout_secs = 5
# statement_cache_size = 50
# Optional driver-level application context, applied during thin logon. Values
# can carry tenant/session identifiers, so list_profiles and diagnostics redact
# them. If inherited, setting entries here replaces the base list; omit to
# inherit or set app_context = [] in the profile table to clear it.
[[profiles.app_context]]
namespace = "ORACLEMCP_CTX"
key = "tenant_id"
value = "tenant-123"
[[profiles.app_context]]
namespace = "ORACLEMCP_CTX"
key = "request_id"
value = "req-456"
[profiles.session_identity]
# Optional: all values are profile-local and are not shown by list_profiles.
# oracle_connection_info reports these only as redacted field names.
# Edition selection is applied during thin authentication before user SQL.
# edition = "ORA$BASE"
program = "oraclemcp"
machine = "local-workstation"
os_user = "local-operator"
terminal = "agent"
driver_name = "oraclemcp"
module = "oraclemcp"
action = "inspect"
client_identifier = "agent"
client_info = "local-workstation"
Keep the signed JSONL and its <audit path>.anchor sidecar together. Run oraclemcp audit verify /var/lib/oraclemcp/audit/audit.jsonl with the same secret reference to re-walk the hashes, verify the MAC, and detect a truncated tail against that anchor. The unsigned refusal trail is deliberately outside this command and cannot provide those tamper-evidence guarantees; it is the diagnostic floor only while the signed tier is unavailable.
max_level is the profile ceiling; default_level is the starting session level and must not exceed that ceiling. call_timeout_seconds defaults to 30 seconds when omitted. It sets the Oracle driver call timeout for the physical connection and the dispatcher request-budget ceiling for the whole tool call; tools that expose timeout_seconds can tighten that budget for one call but cannot loosen the profile ceiling. Set call_timeout_seconds = 0 only as an explicit opt-out from the driver call timeout; doctor warns on that posture. login_statements and login_script are for profile-local session policy only and are restricted to allowlisted ALTER SESSION SET ... parameters. trusted_session_statements are an explicit profile-owner escape hatch for local session initialization such as DBMS_APPLICATION_INFO, application contexts, or DBMS_OUTPUT; they are never accepted from agent tool calls, and they keep environment-specific conventions in private config rather than in the open-source core. session_release_statements and logoff_statements are the matching teardown hooks for profile-owner cleanup: release hooks run only before a successful pooled call returns its physical session to idle reuse, while failed or cancelled pooled calls are discarded; logoff hooks run immediately before logical Oracle logoff. The oracle_connection_info tool reports allow-listed connection posture (backend, connection strategy, server version, role/open mode, read-only status). Session identity and client topology fields such as os_user, program, machine, terminal, client_driver, module, action, client_identifier, and client_info are redacted by default and appear only as names in redacted_fields when present. The Rust thin backend can still set the connect-time client identity fields (program, machine, os_user, terminal, and driver_name) from profile config, and it applies module, action, client_identifier, and client_info after connect through Oracle session APIs. require_signed_tools = true requires HMAC signatures for operator-defined custom tools on that profile; protected = true implies the same policy. The resolved audit, OAuth HS256, and custom-tool HMAC keys must each contain at least 32 bytes of randomly generated key material.
A few further profile keys are optional:
base = "other_profile": inherit another profile's unset fields. A
child may override any inherited field, including raising max_level above the base's value; base is configuration reuse, not a fleet safety ceiling. To pin a production profile at READ_ONLY, set protected = true (which requires max_level = "READ_ONLY") on that child; do not rely on a READ_ONLY base to constrain it.
[profiles.pool]: local client-side connection reuse settings
(max_size, min_idle, acquire_timeout_secs, statement_cache_size). This enables the hybrid runtime strategy for stdio/direct dispatch and lane-local metadata reads: catalog and metadata tools such as schema/object/source inspection can use bounded stateless read connections, while agent queries, sampled rows, LOB reads, DDL/write previews, transactions, savepoints, temp tables, package globals, login setup, session identity, and DBMS_OUTPUT stay on the pinned main session. Served stateless HTTP routes generated metadata reads through bounded read-worker lanes instead of sharing one pool across lane runtimes. When the stateless surface is live, expect at least a pinned main Oracle session plus stateless pool session(s); oracle_connection_info reports connection_strategy = "pinned_plus_stateless" and the stateless pool details separately. max_size is the knob that caps those additional stateless connections. statement_cache_size is passed to the thin driver's bounded per-connection statement cache where pool-backed reads are used; omit it to keep the driver default. This is separate from DRCP server routing.
[profiles.oci]: OCI-specific connection settings for the underlying driver.
For TCPS/wallet connections, named fields are available for wallet_location, wallet_password_ref, ssl_server_dn_match, ssl_server_cert_dn, and use_sni. A wallet does not imply SNI; omit use_sni for the driver default and opt in only when the endpoint's routing name is a rustls-valid DNS name. Oracle's CPython driver can pass Oracle routing tokens through server_hostname; rustls ServerName cannot, so host-as-SNI may skip that one-negotiation routing fast path without breaking TCPS connectivity. Use the named fields for values that should inherit through profiles, be redacted from diagnostics, or be validated by strict config parsing.
sdu = 32768: optional thin driver Session Data Unit request size. Values are
validated as 512..=65535; omit it to keep the driver's negotiated default.
[profiles.drcp]: Database Resident Connection Pooling server routing.
pooled = true appends server=pooled; connection_class maps to pool_connection_class; purity = "reuse" | "new" maps to pool_purity. Existing connect_string query parameters such as wallet_location are preserved and DRCP parameters are appended with &. Prefer these named fields over raw DRCP query parameters when the values should inherit, validate, and be covered by redaction tests.
[profiles.proxy_auth]: thin proxy authentication.proxy_useris the
account that authenticates with credential_ref; target_schema is the Oracle user granted CONNECT THROUGH. The connect username, if present, must match proxy_user.
[[profiles.app_context]]: driver-level application context triples sent
during thin logon. Use typed namespace / key / value entries instead of raw strings; values are treated as sensitive and omitted from ordinary profile output. A child profile inherits the base list when omitted, replaces the whole list when entries are set, and can clear inherited entries with app_context = [].
read_only_standby = true: mark the target as a read-only standby so the
profile cannot be elevated above READ_ONLY regardless of max_level.
mcp_exposed = false: hide this profile from the MCP agent-facing surface
(E5). This is a per-profile opt-out — a profile is exposed to the agent by default, and setting false hides only that one profile. A hidden profile is invisible to oracle_list_profiles, oracle_switch_profile, oracle_search_objects, and completion (a hidden or guessed name fails closed identically); the operator/CLI (oraclemcp profiles, doctor, --profile) still sees every profile. There is no global flip, and one profile's setting never affects another's. It is a visibility/scoping convenience, not an access control — the real bound on what a profile can do is max_level/protected/DB privileges/the fail-closed classifier. At startup the server logs a behavior-neutral exposure summary to stderr, e.g. MCP exposing 1 profile(s): dev_ro [ReadOnly] (1 hidden via mcp_exposed=false). See docs/configuration.md and the cross-profile exposure threat in docs/threat-model.md.
Then launch:
export ORACLE_APP_PASSWORD='...'
oraclemcp serve --allow-no-auth
Config discovery order is:
$ORACLEMCP_CONFIG$XDG_CONFIG_HOME/oraclemcp/profiles.toml, thenconfig.toml(only when
XDG_CONFIG_HOME is set to an absolute path)
~/.config/oraclemcp/profiles.toml, thenconfig.toml
credential_ref and wallet_password_ref resolve through the same SecretResolver seam as audit and HTTP secrets. Supported forms are env:VAR, file:/path/to/secret, keyring:account / keyring:service/account, and the future vault:path seam (fail-closed unless wired). literal:value is for local development only and is rejected when protected = true. File-backed secrets are bounded to 64 KiB and must resolve without following a link to a regular file. On Unix, the file must be owned by the service user, have one hard link, and grant no permissions to group or other users.
The current oraclemcp thin adapter fails explicitly for auth/features it cannot serve end-to-end safely, such as external wallet auth without username/password, autonomous OCI SDK/resource-principal token minting, and Kerberos/RADIUS auth. These appear as structured unsupported diagnostics in oraclemcp doctor --online --profile <profile> and MCP error envelopes; the binary does not silently fall back to thick mode.
OCI IAM database-token auth
use_iam_token = true under [profiles.oci] resolves a pre-fetched database token (a JWT) from exactly one source: token_env, token_file, token_exec, or the built-in ORACLEMCP_IAM_TOKEN when no explicit source is set. The token is injected through the thin driver's access-token connect path and is refused over a non-TCPS transport before it can reach the driver; token_exec is also refused before spawn on non-TCPS. Token values are never persisted, rendered, or logged. iam_config_profile still only parses and is reserved for a future autonomous OCI SDK/resource-principal token source. Real-ADB acceptance remains an operator smoke gate; autonomous OCI SDK minting/refresh remains deferred (bead k6q.9).
Thin result conversion materializes driver-side locators and cursors before serializing tool output: CLOB/BLOB/BFILE locators are read with the query LOB caps, and valid REF CURSOR values or implicit result sets are returned as nested objects containing child columns, rows, row_count, fetched_count, and truncated metadata. Nested cursor materialization has separate row, cell, byte, and depth caps, and unsupported shapes remain explicit instead of silently flattening or guessing. The same versioned OracleCell.structured payload is the catalog-snapshot contract for the optional embedded plsql-intelligence engine: a catalog value is either preserved in a documented structured representation or reported as a typed unsupported marker with provenance, never as an ordinary-looking placeholder string. The separate plsql-mcp server is deprecated.
To live-verify driver-level application context against Oracle 23ai/FREE, create an application context namespace in the test database, configure matching [[profiles.app_context]] triples, then query SYS_CONTEXT('<namespace>', '<key>') through oracle_query or run the optional live test with ORACLEMCP_TEST_APP_CONTEXT='namespace:key:value;namespace:key2:value2'. Invalid or unauthorized context namespaces should fail at connect time with a structured Oracle server error rather than falling back to post-connect SQL.
To live-verify edition selection against Oracle 23ai/FREE, create or reuse a valid edition, set [profiles.session_identity].edition, connect with that profile, and query SYS_CONTEXT('USERENV','CURRENT_EDITION_NAME') through oracle_query or oracle_connection_info. Invalid or unauthorized editions should fail during connect/authentication with a structured Oracle server error; oraclemcp must not silently fall back to the database default edition.
Profile/config regression commands:
# Local, non-secret profile parsing/redaction/setup checks.
cargo test -p oraclemcp-config -p oraclemcp-core profile -- --nocapture
cargo test -p oraclemcp setup_payload_is_generic_and_client_ready -- --nocapture
cargo test -p oraclemcp --test cli setup_write_round_trips_profiles_through_config_ops -- --nocapture
cargo test -p oraclemcp profiles_json_reports_non_secret_metadata -- --nocapture
# Live Oracle 23ai/FREE thin profile/config matrix.
# Required: ORACLEMCP_TEST_DSN, ORACLEMCP_TEST_USER, ORACLEMCP_TEST_PASSWORD.
# Optional: ORACLEMCP_TEST_WALLET_LOCATION, ORACLEMCP_TEST_WALLET_PASSWORD,
# ORACLEMCP_TEST_SSL_SERVER_DN_MATCH, ORACLEMCP_TEST_SSL_SERVER_CERT_DN,
# ORACLEMCP_TEST_USE_SNI, ORACLEMCP_TEST_PROXY_USER,
# ORACLEMCP_TEST_PROXY_TARGET_SCHEMA, ORACLEMCP_TEST_EDITION,
# ORACLEMCP_TEST_APP_CONTEXT, ORACLEMCP_TEST_DRCP=1,
# ORACLEMCP_TEST_DRCP_CLASS.
cargo test -p oraclemcp-db --features live-xe --test live_oracle -- --nocapture
# Faster profile-only smoke subset.
cargo test -p oraclemcp-db --features live-xe live_profile_config -- --nocapture
# Heavy live load/soak (latency p50/p95/p99, leak/balance/drain). Additionally
# opt-in via ORACLEMCP_LIVE_XE=1 on top of the same ORACLEMCP_TEST_* connection
# env. See docs/performance-footprint.md.
ORACLEMCP_LIVE_XE=1 \
ORACLEMCP_TEST_DSN=localhost:1521/FREEPDB1 \
ORACLEMCP_TEST_USER=... ORACLEMCP_TEST_PASSWORD=... \
cargo test -p oraclemcp-db --test load_soak -- --ignored --nocapture
# Structured e2e harness and JSON-line logs for acceptance beads.
bash scripts/e2e/run_all.sh --log --dry-run
bash scripts/e2e/run_all.sh --log
Start a throwaway Oracle FREE 23ai database for the live suite with Docker (it provides FREEPDB1 on :1521):
docker run -d --name oracle-free -p 1521:1521 \
-e ORACLE_PASSWORD=<pw> gvenzl/oracle-free:23-slim
If serve --profile <name> is provided, it overrides default_profile. If neither is set and exactly one profile exists, that sole profile is used.
Agents can inspect available profiles with oracle_list_profiles and reconnect the running MCP server with oracle_switch_profile. A failed switch leaves the current connection in place. oraclemcp serve --profile <name> fails fast when the profile or config cannot be resolved. Without an explicit profile, startup keeps discovery available even when the default live connection cannot be opened; live database calls then return structured tool errors instead of crashing the MCP server.
Operator-defined read-only tools
Operators can expose environment-specific read helpers without forking the server by placing TOML files in ~/.config/oraclemcp/tools.d/*.toml. Set ORACLEMCP_TOOLS_DIR to use a different directory. Definitions are loaded and advertised when serve starts, then revalidated before oracle_switch_profile replaces the active connection; malformed files fail closed instead of silently disappearing.
[[tool]]
name = "app_customer_lookup"
description = "Lookup customer rows by id"
sql = "SELECT id, name, status FROM app_customers WHERE id = :id"
output_mode = "rows"
[[tool.params]]
name = "id"
type = "integer"
required = true
description = "Customer id"
Custom tool SQL uses named binds (:id above). Agent-supplied values are typed from params and bound by name; they are never interpolated into SQL text. The binary loads definitions the classifier deems safe for the active profile ceiling and binds them to the same runtime gate as operator-facing execution tools. Read-only tools execute on a dedicated read-only executor. Write/DDL/PL/SQL tools run through oracle_execute-equivalent semantics: statement-level classification, session/profile gating, preview token verification when required, rollback-by-default for non-confirmed DML, and write-intent/audit recording on committed effects. Unproven package call definitions are rejected by gate policy as forbidden.
On protected profiles, every custom tool must carry a valid HMAC signature. Set ORACLEMCP_CUSTOM_TOOLS_HMAC_KEY in the server environment to verify signed definitions. On unprotected profiles, unsigned tools are allowed for local use; if any definition includes a signature, the same key is required and invalid signatures are rejected. The key must contain at least 32 bytes of randomly generated key material.
Sign local tool definitions from the same binary:
export ORACLEMCP_CUSTOM_TOOLS_HMAC_KEY='...'
oraclemcp sign-tool ~/.config/oraclemcp/tools.d/customer.toml --tool app_customer_lookup --write
By default the command prints signature values to place into matching [[tool]] blocks; it does not print the HMAC key. Pass --write (alias --in-place) to atomically place each generated signature in its matching [[tool]] block — including when the file ends with [[tool.params]].
Custom-tool signatures use the self-identifying oraclemcp-custom-tool:v2:hmac-sha256:… format. Version 2 authenticates every semantic and agent-visible definition field, including nested parameter descriptions, parameter order, and output_mode; only the signature envelope itself is excluded. Bare 64-hex signatures emitted by older releases are not accepted when present, and protected-profile startup fails with re-sign guidance instead of silently falling back to the incomplete legacy format. During upgrade, run oraclemcp sign-tool for each tools file and replace every legacy signature value before restarting a protected profile.
Building and testing
oraclemcp builds on a single pinned Rust nightly (nightly-2026-05-11, recorded in rust-toolchain.toml). Two independent things need it: asupersync 0.3.9's nightly-outcome-try feature (try_trait_v2 + try_trait_v2_residual), which is opt-in but on by default and reaches us via the oraclemcp-driver-cx 0.9.2 dependency; and, on Windows only, windows_by_handle. The pin is build-time only: the shipped binary has no runtime dependency on nightly. See docs/toolchain.md for the full rationale and the re-pin runbook.
rustup toolchain install nightly-2026-05-11 --component rustfmt --component clippy
# Build and run the full test matrix (the workspace's rust-toolchain.toml picks the pin).
cargo build --workspace
cargo test --workspace --all-targets
cargo test --workspace --doc
Before a release, the same gates CI enforces must pass on the pinned toolchain (full list and a copy-pasteable checklist in docs/release-checklist.md):
cargo fmt --all -- --check # formatting
cargo clippy --workspace --all-targets -- -D warnings # lint, warnings = errors
cargo deny check # advisories / licenses / bans / sources
bash scripts/oraclemcp_agent_surface_lint.sh # no arbitrary routine MCP surface
bash scripts/oraclemcp_driver_seam_lint.sh # driver-adapter seam stays one file
bash scripts/oraclemcp_honesty_grep.sh # no over-claiming framing
bash scripts/oraclemcp_api_lock.sh # public API lock (no unreviewed surface drift)
RELEASE_TAG=vX.Y.Z bash scripts/release_preflight.sh # release metadata sync
Running the live suite
The default test run needs no database. The live thin paths run against a real Oracle 23ai via the unified ORACLEMCP_TEST_* env; a throwaway Oracle FREE is enough (provides FREEPDB1 on :1521):
docker run -d --name oracle-free -p 1521:1521 \
-e ORACLE_PASSWORD=<pw> gvenzl/oracle-free:23-slim
export ORACLEMCP_TEST_DSN=localhost:1521/FREEPDB1
export ORACLEMCP_TEST_USER=... ORACLEMCP_TEST_PASSWORD=...
cargo test -p oraclemcp-db --features live-xe --test live_oracle -- --nocapture
# Heavy load/soak is additionally opt-in via ORACLEMCP_LIVE_XE=1.
ORACLEMCP_LIVE_XE=1 cargo test -p oraclemcp-db --test load_soak -- --ignored --nocapture
The full live env block (wallet/TCPS, proxy, DRCP, edition, app-context vars) is in the live test commands above and in docs/operations.md §5.7. Connection-profile fields are documented in docs/configuration.md.
Supported Oracle versions
| Dimension | Support | |---|---| | Database version | Tested against Oracle Database 23ai, including the free Oracle FREE 23ai image (gvenzl/oracle-free:23-slim, FREEPDB1). The pure-Rust thin oraclemcp-driver-cx driver speaks the Oracle Net protocol directly — no Instant Client or ODPI-C. | | EZConnect | Supported (host:port/service, plus EZConnect-Plus `tcps://…?wallet_location=…











