{
  "markdown": "<p align=\"center\">\n  <img src=\".github/assets/hero.svg\" alt=\"oraclemcp: governed, least-privilege Oracle Database MCP server in pure Rust\" width=\"100%\">\n</p>\n\n<p align=\"center\">\n  <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>\n  <a href=\"https://crates.io/crates/oraclemcp\"><img src=\"https://img.shields.io/crates/v/oraclemcp.svg\" alt=\"crates.io\"></a>\n  <a href=\"#license\"><img src=\"https://img.shields.io/badge/license-Apache--2.0%20OR%20MIT-blue.svg\" alt=\"license\"></a>\n  <img src=\"https://img.shields.io/badge/unsafe-forbidden-success.svg\" alt=\"forbid(unsafe_code)\">\n  <img src=\"https://img.shields.io/badge/rustc-nightly--2026--05--11-orange.svg\" alt=\"nightly-2026-05-11\">\n</p>\n\n> **Governed, least-privilege Oracle Database access for AI agents — in pure Rust.**\n\n`oraclemcp` is a [Model Context Protocol](https://modelcontextprotocol.io) 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)]`.\n\n> _An independent open-source project; not affiliated with Oracle. For Oracle's own MCP servers, see [oracle/mcp](https://github.com/oracle/mcp)._\n\n## Install, service, dashboard\n\nOne line installs or updates `oraclemcp` on macOS and Linux. It works as pasted\nfor a human terminal and for a non-interactive agent run:\n\n```sh\ncurl -fsSL \"https://raw.githubusercontent.com/MuhDur/oraclemcp/main/install.sh?$(date +%s)\" | bash\n```\n\nThe hosted script fetch includes a cache buster so stale CDN/proxy copies do not\nhide installer updates. This command installs the latest published release; add\n`--version X.Y.Z` (or `vX.Y.Z`) to pin a specific release instead. Later\nexamples that contain `...`, `<pw>`, `<profile>`, or placeholder env values are\ntemplates: replace those placeholders before running them.\n\nThe normal command downloads, verifies, and installs into `$HOME/.local` unless\nyou pass `--prefix`. It requires the SHA-256 digest check, verifies the cosign\nblob signature and provenance attestation when cosign is installed, and installs\n`oraclemcp` plus the short `om` alias. Missing cosign is a visible\nauthenticity-unverified posture by default; use `--verify require` when your\nenvironment requires cosign to be present.\n\nIn an interactive terminal, the installer then offers a short guided flow:\nappend the binary directory to `PATH`, run `doctor`, offer zero-config database\ndiscovery from `tnsnames.ora`, print an MCP client snippet, and optionally\ninstall the loopback service. In a pipe, CI job, or agent run, it never prompts,\nnever scans, and never starts a service; it installs the binary and prints the\nexact `PATH` line plus next steps on stderr. Every install finishes with next\nsteps on stderr: discover databases, run `doctor`, write the starter profile,\nand generate MCP client snippets.\n\n### Zero-config onboarding\n\n`oraclemcp setup --discover` finds every database defined in your `tnsnames.ora`\nand writes one **read-only** connection profile per net-service — through the\nsame governed config-ops path (timestamped backup, atomic write, strict\nre-validation) used everywhere else. It is **consent-gated**: an interactive run\nasks before it scans and again before it writes; a non-interactive run without\n`--discover-tns` (or `--yes`) refuses with exit code 2 and scans nothing. It\nwrites **no secrets to disk** (each profile references an environment variable,\n`env:ORACLE_<NAME>_PASSWORD`, that you export yourself), keeps every profile\ncapped at `READ_ONLY`, and is **idempotent and non-destructive**: existing\nprofiles and hand edits are preserved, only new databases are added. When no\n`tnsnames.ora` is found it falls back to the minimal starter profile so you\nstill boot. Add\n`--json` for a names-only agent report, or `--dry-run` to preview without\nwriting. Run `oraclemcp doctor` afterwards to see exactly which credentials\nremain to be set. Full contract: `docs/tns-discovery-onboarding.md`.\n\nRe-running the same one-liner is the update path. Re-running the same verified\narchive is a no-op for identical installed files; re-running with a newer target\nupdates atomically after backing up the previous binary. A downgrade is refused unless you pass `--force`.\n\nOperator migration notes for the current field-hardening train:\n[`docs/oraclemcp-091-field-hardening-notes.md`](docs/oraclemcp-091-field-hardening-notes.md).\nConfig-migration runbooks introduced in 0.8.0 still apply when upgrading from an\nolder release:\n[`docs/upgrading-to-0.8.0.md`](docs/upgrading-to-0.8.0.md),\n[`docs/downgrading-0.8.0-to-0.7.2.md`](docs/downgrading-0.8.0-to-0.7.2.md),\nand [`docs/feature-rollout-0.8.0.md`](docs/feature-rollout-0.8.0.md).\n\nUse the dry-run command first when you want a preview: it prints the archive,\nverification inputs, files, service plan, client-registration plan, and\ninstaller lock path, then exits before downloading, verifying, writing files, or\ntouching the service manager. Dry-run exists for review and automation plans;\nthe normal command above is the install/update command.\n\n### Advanced install paths\n\nPreview the Linux/macOS host plan without changing the machine:\n\n```sh\ncurl -fsSL \"https://raw.githubusercontent.com/MuhDur/oraclemcp/main/install.sh?$(date +%s)\" | bash -s -- --dry-run\n```\n\nFrom an installed binary, preview or run the same update path:\n\n```sh\noraclemcp --json self-update --dry-run\noraclemcp self-update --no-service\n```\n\nOn Windows, download and run the PowerShell installer:\n\n```powershell\niwr -UseBasicParsing https://raw.githubusercontent.com/MuhDur/oraclemcp/main/install.ps1 -OutFile install.ps1\npowershell -ExecutionPolicy Bypass -File .\\install.ps1 -DryRun\npowershell -ExecutionPolicy Bypass -File .\\install.ps1\n```\n\nThe Windows installer accepts the same release operations: `-Update` for the\nexplicit update path, `-NoService` to suppress service prompts, and\n`-Verify prefer`, `-Verify require`, or `-Verify checksum-only` for the\nverification posture. `prefer` installs after a hard SHA-256 check when cosign\nis missing; `require` fails without cosign.\n\n```powershell\npowershell -ExecutionPolicy Bypass -File .\\install.ps1 -Update -NoService\n```\n\nFor air-gapped hosts, stage five inputs: the release archive, its `.sha256`,\n`.sigstore.json`, and `.attestation.sigstore.json` siblings, plus a Sigstore\n`trusted_root.json` obtained independently on a connected staging host. With a\ntrusted Cosign v3 installation, refresh that root through Sigstore's TUF\nmetadata before moving it across the air gap:\n\n```sh\ncosign trusted-root create --with-default-services --out sigstore-trusted-root.json\n```\n\nThen require authenticity and provenance verification during the offline\ninstall:\n\n```sh\nbash install.sh \\\n  --offline ./oraclemcp-x86_64-unknown-linux-musl.tar.gz \\\n  --version 0.10.0 \\\n  --verify require \\\n  --trusted-root ./sigstore-trusted-root.json\n```\n\n```powershell\npowershell -ExecutionPolicy Bypass -File .\\install.ps1 `\n  -Offline .\\oraclemcp-x86_64-pc-windows-msvc.zip `\n  -Version 0.10.0 `\n  -Verify require `\n  -TrustedRoot .\\sigstore-trusted-root.json\n```\n\nThe trusted root is trust material, not another self-authenticating release\nasset. Provision and protect it separately from the archive bundle. Offline\nCosign verification fails closed when it is absent; `checksum-only` remains an\nexplicit integrity-only posture.\n\nThe release installer does not silently fall back from a missing release archive\nto a source build. Use `--source` explicitly when you want `cargo install`\ninstead of the verified archive path.\n\nOn Linux the installer auto-detects the static musl build, which runs everywhere\n(including WSL2). The published glibc tarballs are also installable, but only by\nexplicit request: `--target x86_64-unknown-linux-gnu` (or\n`aarch64-unknown-linux-gnu`).\n\nUninstall is preview-first and idempotent. Service removal remains an explicit\nservice-manager mutation:\n\n```sh\nbash install.sh --uninstall --dry-run\nbash install.sh --uninstall --yes\nbash install.sh --uninstall --service --yes\n```\n\n```powershell\npowershell -ExecutionPolicy Bypass -File .\\install.ps1 -Uninstall -DryRun\npowershell -ExecutionPolicy Bypass -File .\\install.ps1 -Uninstall -Yes\n```\n\nInstall the local service only with explicit consent. Keep it on loopback unless\nyou deliberately configure remote HTTP, and use service-owned client credentials,\nOAuth, or mTLS for HTTP MCP clients. For Windows service install, the PowerShell\ninstaller also requires explicit consent.\n\n```sh\noraclemcp --json service install --dry-run --profile db_ro --listen 127.0.0.1:7070 --client-credentials\noraclemcp service install --yes --profile db_ro --listen 127.0.0.1:7070 --client-credentials\noraclemcp --json clients issue --label claude --scope oracle:read\n```\n\n```powershell\npowershell -ExecutionPolicy Bypass -File .\\install.ps1 -Service -Yes -Profile db_ro\n```\n\nRequest a listener-bound pairing URL plus a one-time code, open the printed URL,\nand paste the code into the form it serves:\n\n```sh\nom dashboard\n```\n\nThe printed URL carries **no secret**, so it is safe in browser history, in a\n`Referer`, and in the view of any extension holding `tabs`/`webNavigation`\npermission. The one-time code is accepted only from the pairing form's POST body\n— never from a URL query or fragment — and the CLI never hands either to a\ndesktop launcher (where process argv could expose it). The dashboard uses a\none-time loopback pairing ticket bound to the exact live listener instance and\nscheme/host/port, then an HttpOnly, SameSite=Strict cookie plus CSRF and\nroute-scoped action tickets. The cookie is\n`Secure` under native TLS or explicit trusted HTTPS termination; the only\nnon-Secure exception is server-observed loopback HTTP, and remote plaintext\nrequests never receive privileged browser cookies. Browser\nrequests do not supply the database Subject: the server derives the Subject from\nthe authenticated transport principal, session, and lane context. Authenticated\nHTTP sessions run on isolated per-principal lanes with their own Oracle\nconnection, operating level, grants, cancellation, and audit context. Intentional\n`--allow-no-auth` HTTP development uses one anonymous lane; stdio remains the\nsingle local client path.\n\nOther release channels come from the same signed archive matrix. These channels\ncan lag the GitHub release tag, so use the check command first and install only\nafter it resolves the target version.\n\n```sh\ncargo binstall oraclemcp\ndocker run -i --rm ghcr.io/muhdur/oraclemcp:latest\n```\n\nPending registry-backed channels:\n\n```sh\nbrew info MuhDur/oraclemcp/oraclemcp\nwinget search --id MuhDur.oraclemcp --exact\n```\n\nAfter the relevant check resolves the target version, these commands are\ncopy-pasteable:\n\n```sh\nbrew install MuhDur/oraclemcp/oraclemcp\nwinget install --id MuhDur.oraclemcp --exact\n```\n\nAn npm/npx channel is not offered. Install with the one-line installer above, or\n`cargo binstall oraclemcp`, the GHCR Docker image, or the Homebrew/winget\nchannels once they resolve.\n\n## Why oraclemcp\n\n- **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-only `SELECT`/`WITH` and dictionary introspection. Non-read execution is isolated in `oracle_execute`, bounded by profile `max_level`/`default_level`, rollback-by-default for DML, and explicit-confirm-before-commit. Temporary elevation through `oracle_set_session_level` can 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 an `OperatingLevelTooLow` or `ForbiddenStatement` envelope and a suggested safe alternative.\n- **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 structured [`ErrorEnvelope`](crates/oraclemcp-error)s with machine-stable classes, fuzzy suggestions, and next-step hints, not bare strings. A zero-arg `oracle_capabilities` tool lets an agent discover the surface; MCP resources expose the capability/tool documents plus schema/object read templates; and an offline build degrades to a `RuntimeStateRequired` contract instead of crashing.\n- **Pure Rust, no `unsafe`.** Every crate is `#![forbid(unsafe_code)]`; the fail-closed classifier carries a differential cargo-fuzz target.\n- **Two transports.** stdio (default) and Streamable HTTP (`--listen`) with\n  fail-closed auth defaults, optional OAuth bearer enforcement, and native\n  rustls TLS/mTLS.\n\n## Source builds and runtime requirements\n\nThis branch is pinned to **`nightly-2026-05-11`** and has no stable MSRV. The\npin is required, for two independent reasons: this checkout resolves\n**asupersync 0.3.9**, whose\n`nightly-outcome-try` feature enables `#![feature(try_trait_v2)]` and\n`try_trait_v2_residual` inside asupersync (it is opt-in, but in asupersync's\ndefault feature set, and reaches us through the `oraclemcp-driver-cx` dependency), and on\n**Windows** `oraclemcp-core` additionally needs `windows_by_handle`. The pinned\n`oraclemcp-driver-cx` 0.9.2 driver's own source is stable-clean — it is its asupersync\ndependency declaration that pulls the nightly feature in.\n[`docs/toolchain.md`](docs/toolchain.md) has the exact mechanism. The\nrepository's `rust-toolchain.toml` selects the pin for local builds. Use the release installer above when you want\nthe prebuilt binary; use `cargo install` only when you intentionally want a\nsource build.\n\n```sh\nrustup toolchain install nightly-2026-05-11 --component rustfmt --component clippy\n```\n\nDirect source install:\n\n```sh\ncargo +nightly-2026-05-11 install oraclemcp\n```\n\nLive database access is built in through the pure-Rust thin `oraclemcp-driver-cx` driver.\n\n**Runtime requirements** for live database access:\n\n- Optionally `TNS_ADMIN` pointing at a directory with `tnsnames.ora` if you connect by net-service name.\n\nNo Oracle Instant Client, ODPI-C library, or C toolchain is required by the\ndriver.\n\nUse `oraclemcp --json doctor` to verify the binary and offline setup,\n`oraclemcp --json doctor --profile <profile>` to inspect non-secret profile\nmetadata without resolving secrets, and\n`oraclemcp --json doctor --online --profile <profile>` to add live\nconnectivity, authentication, role/open-mode, standby, and privilege checks.\n`oraclemcp doctor oauth --token <JWT>` validates one supplied OAuth token against\nthe local resource-server config before any live connection; the token is never\nlogged, persisted, or rendered.\nDoctor output is safe to paste into agent sessions: it omits connect strings,\nusernames, `credential_ref` values, passwords, proxy identities, wallet\npasswords, IAM tokens, wallet paths, and server DNs while keeping structured\nfailure classes and ORA codes visible.\n\nGenerate generic local setup templates for profiles, wrappers, and MCP client\nsnippets:\n\n```sh\noraclemcp --json setup --profile db_ro\n```\n\nTo create a minimal starter profiles file directly, use the same config-ops\nbackend the dashboard uses. This validates the draft, writes a backup,\natomically replaces the target, and reports the reload/rollback metadata\nwithout echoing the raw profile TOML:\n\n```sh\noraclemcp --json setup --write --profile db_ro\n```\n\n**Docker:** a ready-to-run thin-driver image, published to GHCR and listed in the [MCP registry](https://registry.modelcontextprotocol.io) 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:\n\n```sh\nif [ \"$(id -u)\" -eq 0 ]; then\n  printf '%s\\n' 'Refusing to run oraclemcp as root.' >&2\nelse\n  container_state=\"${XDG_STATE_HOME:-$HOME/.local/state}/oraclemcp-container\"\n  mkdir -p \"$container_state\" && chmod 0700 \"$container_state\" &&\n    docker run -i --rm --user \"$(id -u):$(id -g)\" \\\n      -v \"$HOME/.config/oraclemcp:/home/oraclemcp/.config/oraclemcp:ro\" \\\n      -v \"$container_state:/home/oraclemcp/.local/state/oraclemcp\" \\\n      -e ORACLE_APP_PASSWORD \\\n      ghcr.io/muhdur/oraclemcp:latest\nfi\n\ndocker run -i --rm ghcr.io/muhdur/oraclemcp:latest  # tool surface only (no DB)\n```\n\nAn optional PL/SQL intelligence image is available from the manual Docker\nworkflow. It is the same server compiled with `--features plsql-intelligence`;\nit can start without a database connection and advertises the offline\n`oracle_plsql_*` tools immediately. Live PL/SQL tools still require a profile.\n\n```sh\ndocker run -i --rm ghcr.io/muhdur/oraclemcp:plsql-intelligence-latest --json info\ndocker run -i --rm ghcr.io/muhdur/oraclemcp:plsql-intelligence-latest capabilities\n```\n\nLocal feature-image builds resolve the PL/SQL engine crates from crates.io:\n\n```sh\ndocker buildx build \\\n  --target runtime-plsql-intelligence \\\n  -t oraclemcp:plsql-intelligence .\ndocker run -i --rm oraclemcp:plsql-intelligence --json info\n```\n\n> The Docker image and crates are Apache-2.0 OR MIT and do not redistribute Oracle Instant Client.\n\nWire it into an MCP client (e.g. Claude Desktop) over stdio:\n\n```json\n{\n  \"mcpServers\": {\n    \"oracle\": {\n      \"command\": \"oraclemcp\",\n      \"args\": [\"serve\", \"--profile\", \"db_ro\", \"--allow-no-auth\"]\n    }\n  }\n}\n```\n\nFor Codex-style TOML config, the same command is:\n\n```toml\n[mcp_servers.oracle]\ncommand = \"oraclemcp\"\nargs = [\"serve\", \"--profile\", \"db_ro\", \"--allow-no-auth\"]\n```\n\nOr run it directly:\n\n```sh\noraclemcp serve                      # stdio (default); --allow-no-auth for local dev\noraclemcp --json clients issue --label claude --scope oracle:read  # shown-once HTTP bearer\noraclemcp serve --listen 127.0.0.1:7070 --client-credentials --profile db_ro\nclaude mcp add oracle --transport http http://127.0.0.1:7070/mcp --header \"Authorization: Bearer <bearer>\"\noraclemcp serve --listen 127.0.0.1:7070 --allow-no-auth   # local HTTP dev only\noraclemcp --json setup --profile db_ro    # generic onboarding templates\noraclemcp --json setup --write --profile db_ro  # write starter profiles via SCFG\noraclemcp capabilities               # the advertised tool surface + feature tiers (JSON)\noraclemcp --json profiles            # configured profile names and non-secret metadata\noraclemcp doctor                     # offline diagnostics (thin driver, TNS/wallet, classifier, NLS)\noraclemcp doctor --profile dev_ro    # inspect profile metadata offline\noraclemcp doctor --online --profile dev_ro  # include live connectivity/auth/role/privilege checks\noraclemcp info                       # build info: version, tools, transports, thin DB\noraclemcp robot-docs guide           # compact in-binary guide for agents\noraclemcp completions bash           # shell completions: bash, zsh, fish, powershell\noraclemcp --json service install --dry-run --profile db_ro  # preview systemd/launchd/Windows service changes\noraclemcp service install --yes --client-credentials --profile db_ro\noraclemcp --json service status       # inspect service-manager state\noraclemcp --json service logs         # inspect recent service logs\noraclemcp --json service backup --dry-run  # preview state+config backup\noraclemcp --json service restore /path/to/backup --dry-run  # verify audit chain before restore\noraclemcp dashboard                   # open the local dashboard through a one-time pairing URL\n```\n\n`--json` is a visible alias for `--robot-json` and keeps stdout as a single\nmachine-readable JSON object.\n\n### Stdio init-token clients\n\n`ORACLEMCP_STDIO_TOKEN` (or `serve --stdio-token`) enables a handshake token\nfor a **custom MCP client** that controls the raw `initialize` request. The\nclient must send the shared token as a JSON string at exactly\n`params._meta[\"oraclemcp/initToken\"]`; a missing key or a non-string value is\nreported as missing. This is not a generic Claude/Codex-style configuration\nsnippet: mainstream MCP client configuration surfaces do not provide a way to\ninject `initialize` metadata. If the client cannot control that frame, keep\nstdio local and use `--allow-no-auth` deliberately, or use authenticated HTTP\ninstead.\n\nRelease archives also include `om` (`om.exe` on Windows) as an argv0-aware\nshort alias. `om dashboard` is equivalent to `oraclemcp dashboard` and uses the\nshort name in CLI help and dashboard diagnostics when invoked through that\nalias.\n\n`oraclemcp service install` targets the platform user service manager: systemd\n`--user` on Linux, launchd on macOS, and Windows services on Windows. Mutating\nservice operations (`install`, `uninstall`, `restart`, `backup`, `restore`) require `--yes`;\n`--dry-run` emits the exact file and command plan without changing the host.\nGenerated service definitions include bounded host caps for the 64-lane default:\nsystemd uses `Type=notify`, `NotifyAccess=main`, `Restart=on-failure`,\n`LimitNOFILE=65536`, `TasksMax=512`, `MemoryMax=2G`, and\n`OOMScoreAdjust=100`; launchd uses `KeepAlive` plus file/process\n`SoftResourceLimits`; Windows configures automatic start and restart-on-failure\nthrough `sc.exe`. `oraclemcp --json doctor` reports those configured caps plus\nthe effective open-file, task, memory-cgroup, and OOM caps visible to the\ncurrent process. `service backup` snapshots the XDG service state directory plus\nthe resolved profiles config into a new manifest directory; `service restore`\nverifies the backed-up audit hash-chain before stopping the service, restoring\nfiles, and starting it again.\nStreamable HTTP auth rules are unchanged for service mode: configure\nservice-owned per-client credentials, OAuth, or mTLS with registered client leaf\nfingerprints, or pass `--allow-no-auth` only for intentional local development.\nThe HTTP service also owns a private `service-instance.json` lock in its state\nroot (`$XDG_STATE_HOME/oraclemcp`, or `$HOME/.local/state/oraclemcp` when XDG\nis unset). A second `serve --listen` process using that same state root refuses\nto start and reports the existing pid/listen metadata instead of silently\ntaking over another port or socket. For intentionally independent instances,\ngive each process a distinct `XDG_STATE_HOME` and listener port; that also\nseparates their service-owned credentials, audit records, and durable state.\n\nThe browser dashboard is paired separately even on loopback. `oraclemcp\ndashboard` creates a 0600 one-time ticket under the user runtime directory and\nprints a secret-free `/dashboard/pair` URL alongside a one-time code. That URL\nserves a script-free form; submitting the code POSTs it in the request body, and\nthe server exchanges it for an HttpOnly, SameSite=Strict dashboard cookie. The\ncode is never read from the request target — a `/dashboard/pair?ticket=...` URL\nis refused outright without consuming the ticket — so the bootstrap secret\ncannot be recovered from browser history, a `Referer`, an access log, or a\nbrowser extension watching navigations. The cookie is `Secure` under\nnative TLS or `[http].trusted_https_termination = true`; non-Secure cookies are\nlimited to server-observed loopback HTTP. Forwarded scheme headers are ignored,\nand remote plaintext requests do not receive privileged browser cookies. The\nticket expires in 60 seconds\nand is single-use; dashboard POSTs also require same-origin headers, a CSRF\ntoken, and a route-scoped action ticket. The dashboard origin threat-model\naddendum documents why the browser contract stays fail-closed:\n[`docs/dashboard-origin-threat-model-addendum.md`](docs/dashboard-origin-threat-model-addendum.md).\n\nThe dashboard Workbench is not a terminal or SQL shell. It is disabled unless\n`[http].dashboard_workbench = true`; the Database Explorer remains available\nfor governed metadata browsing when it is off. Classify and preview actions\nforward to `oracle_preview_sql`, read execution forwards to `oracle_query`, and\nguarded DML forwards to `oracle_execute` with the same single-use confirmation\ngrant and audit path agents use. Browser-originated DDL/Admin apply remains\nblocked; DDL can be previewed, but applying it requires a non-browser operator\npath.\nWhen compiled with `plsql-intelligence`, the Workbench IDE panel also exposes\nthe static `oracle_plsql_parse`, `oracle_plsql_analyze`,\n`oracle_plsql_lineage`, `oracle_plsql_sast`, `oracle_plsql_doc`, and\n`oracle_plsql_what_breaks` tools for source navigation, dependency, lint, doc,\nand impact previews; live snapshot/blast-radius tools remain outside the\nbrowser allowlist.\nThe Reviews board stores profile-scoped Change Proposals as service-owned SQL\ntemplates plus captured binds, then applies them by re-classifying each\ntemplate and forwarding through the same guarded action route; stored proposal\nverdicts are never authorization inputs. For source-replaceable\n`CREATE OR REPLACE` DDL, proposal apply captures the prior source into\ncontent-addressed service files before dispatch when the current source is\nvisible. `/operator/v1/source-history` lists source-free snapshot metadata, and\ndashboard revert creates a normal DDL Change Proposal from the stored snapshot\ninstead of bypassing review, confirmation, or profile ceilings.\nThe Explorer page includes global search across visible schemas: object-name\nmatches use `oracle_search_objects` with all object types, and source-text\nmatches use `oracle_search_source`; both are sent through the same guarded\noperator action route as the rest of the dashboard.\nThe Reviews page can also compare two supplied schema snapshots and export a\nreviewable migration script. The diff view omits raw DDL and shows hashes/counts;\nany executable export step must be drafted into the normal Change Proposal board\nbefore apply, where the server re-classifies and re-checks the statement.\n\nThe Streamable HTTP transport (`--listen`) fails closed. It starts only when\nservice-owned per-client credentials, OAuth bearer enforcement, mTLS\nclient-certificate verification, or `--allow-no-auth` is supplied, and mTLS\nrequests become application principals only through registered leaf\nfingerprints. It refuses any non-loopback bind unless\n`ORACLEMCP_HTTP_ALLOW_REMOTE=1` is set. Per-client credentials are one bearer\nper MCP client; the bearer is shown once by `oraclemcp clients issue` or\n`oraclemcp clients rotate`, while `clients.json` stores only salted hashes:\n\n```sh\noraclemcp --json clients issue --label claude --scope oracle:read\noraclemcp serve --listen 127.0.0.1:7070 --client-credentials --profile db_ro\nclaude mcp add oracle --transport http http://127.0.0.1:7070/mcp --header \"Authorization: Bearer <bearer>\"\noraclemcp --json clients rotate <client_id>\noraclemcp --json clients revoke <client_id>\n```\n\n### Online client-credential lifecycle\n\nFor a running HTTP service, use the dashboard's Client Credentials control or\nthe authorized operator routes: `GET /operator/v1/client-credentials`,\n`POST /operator/v1/client-credentials/rotate`, and\n`POST /operator/v1/client-credentials/revoke`. The two mutation routes accept\n`{\"client_id\":\"...\"}`. Browser calls use the normal dashboard pairing, CSRF,\nand route-action tickets; non-browser calls need the normal `/operator/v1`\nauthority. Do not expose these controls to ordinary MCP clients.\n\nA rotate or revoke is a per-client online lifecycle operation, not a service\nrestart procedure. After its credential-store mutation is authoritative, the\nserver removes that client's HTTP sessions and buffered SSE results, closes its\nstateful lanes, and installs the new credential generation as the admission\nfloor. A request that authenticated with the old bearer but is still creating a\nlane is refused rather than gaining a fresh lane; subsequent requests with the\nold bearer fail authentication. On rotation, retain the one-time replacement\nbearer and reconnect with it to establish a fresh session. Other clients keep\ntheir sessions and bearers. Database credentials and OAuth/audit signing keys\nhave separate restart lifecycles; see the operations guide.\n\nOAuth configuration can come from `profiles.toml` or CLI flags. The resolved\nHS256 secret must be at least 32 bytes (256 bits); use randomly generated key\nmaterial rather than a password or memorable phrase:\n\n```sh\nexport ORACLEMCP_OAUTH_HS256_SECRET='replace-with-a-long-random-secret'\noraclemcp serve --listen 127.0.0.1:7070 \\\n  --oauth-resource http://127.0.0.1:7070/mcp \\\n  --oauth-issuer https://issuer.example.com \\\n  --oauth-authorization-server https://issuer.example.com \\\n  --oauth-required-scope oracle:read \\\n  --oauth-hs256-secret-ref env:ORACLEMCP_OAUTH_HS256_SECRET \\\n  --http-allowed-host 127.0.0.1:7070 \\\n  --http-allowed-origin https://client.example.com\n```\n\nWhen OAuth is enabled, `/.well-known/oauth-protected-resource` stays public,\n`/mcp` requires a valid bearer token, and granted `oracle:*` scopes lower the\nrequest's effective operating ceiling monotonically. `oracle:read` caps the\nrequest at `READ_ONLY`, `oracle:write`/`oracle:execute` at `READ_WRITE`,\n`oracle:ddl` at `DDL`, and `oracle:admin` at `ADMIN`; none of them can raise a\nprofile above its `max_level`, and protected profiles remain `READ_ONLY`.\nJWT bearer tokens must use the RFC 9068 access-token profile: the protected\n`typ` header is `at+jwt` (or `application/at+jwt`), and `iss`, `sub`, `aud`,\n`exp`, `client_id`, `iat`, and `jti` have their required access-token shapes.\nGeneric JWTs and OpenID Connect ID tokens are rejected; there is no implicit\ngeneric-JWT compatibility mode.\n\n### OAuth bearer-token contract\n\n`--oauth-hs256-secret-ref` is a **secret reference**, not an encoded-key\nsetting. The server resolves the reference and uses the resolved value's raw\nUTF-8 bytes as the HS256 key; it does not base64- or hex-decode them. For\nexample, `env:ORACLEMCP_OAUTH_HS256_SECRET` reads that environment variable,\nwhile putting a raw key in the field makes the literal `env:...` text the key.\nThe resolved key must contain at least 32 bytes.\n\nAn accepted access token has `typ: at+jwt` (or `application/at+jwt`), a\nsupported `alg`, and non-empty string `iss`, `sub`, `client_id`, and `jti`\nclaims; numeric `iat`; a future numeric `exp`; and an `aud` that is either the\nconfigured resource string or an array containing it. The issuer must exactly\nmatch `http.oauth.allowed_issuers`. A token supplies scopes as either a\nspace-delimited `scope` string or an `scp` array, and must satisfy every\nnon-empty `http.oauth.required_scopes` entry.\n\nRejected requests keep the response body and `WWW-Authenticate` challenge\ngeneric: a presented but rejected bearer always receives `error=\"invalid_token\"`\nwith no `error_description`. This prevents an unauthenticated caller learning\nwhether a token was malformed, signed correctly but expired, or missing a\nparticular claim. The server records the fixed rejection category in its\noperator audit/security trail; it never records or echoes a bearer token,\nsignature, or untrusted issuer value.\n\nNative TLS uses rustls when `[http.tls]` or `--tls-cert` / `--tls-key` are\nconfigured. Adding `[http.tls.client_ca_path]` or `--mtls-client-ca` requires\nclient certificates (mTLS) verified against that CA, but a CA-verified cert is\nnot an application identity until its leaf DER SHA-256 fingerprint is listed in\n`[http.mtls].client_fingerprints` or passed with `--mtls-client-fingerprint`.\nThe resulting principal key is `mtls:sha256:<hex>`. Server-only TLS encrypts the\ntransport but is not application authentication, so `/mcp` still needs\nper-client credentials, OAuth, or an explicit `--allow-no-auth` development\nopt-in. Non-loopback binds require `ORACLEMCP_HTTP_ALLOW_REMOTE=1` even with\nTLS. Native TLS on a non-loopback listener emits\n`Strict-Transport-Security: max-age=31536000; includeSubDomains`; loopback\nHTTPS deliberately omits HSTS so browser pinning cannot disrupt local HTTP\ndevelopment.\n\nWhen Claude Code connects over HTTPS to a self-signed or private-CA listener,\nstart it with that CA PEM in Node's trust store: `NODE_EXTRA_CA_CERTS=/path/to/private-ca.pem claude`.\n\nConnection profiles are resolved from layered configuration (`oraclemcp-config`); select one with `serve --profile <name>`.\n\n### Connection profiles\n\n> **See also:** [`oraclemcp.example.toml`](oraclemcp.example.toml) is a fully\n> annotated, copy-pasteable config showing every field with its default;\n> [`docs/configuration.md`](docs/configuration.md) is the canonical field\n> reference (types, defaults, precedence, the operating-level ladder, the\n> `mcp_exposed` opt-out, auth modes, and `base` inheritance). The\n> `oraclemcp setup --write` starter is intentionally smaller so it can boot\n> before you add wallet, proxy, DRCP, pool, app-context, or writable-profile\n> settings.\n\n### Signed audit and unsigned refusal trail\n\nFor live database access, create `~/.config/oraclemcp/profiles.toml`:\n\n```toml\nschema_version = 2\ndefault_profile = \"dev_ro\"\n# Optional least-privilege profile for fleet-wide DB observability.\n# monitor_profile = \"monitor_ro\"\n\n[http]\nallowed_hosts = [\"127.0.0.1:7070\"]\nallowed_origins = [\"https://client.example.com\"]\njson_response = true\nstateful = false\ndashboard_workbench = false\n\n[http.oauth]\nresource = \"http://127.0.0.1:7070/mcp\"\nallowed_issuers = [\"https://issuer.example.com\"]\nauthorization_servers = [\"https://issuer.example.com\"]\nrequired_scopes = [\"oracle:read\"]\nhs256_secret_ref = \"env:ORACLEMCP_OAUTH_HS256_SECRET\"\n\n# Optional native HTTPS / mTLS listener.\n# [http.tls]\n# cert_chain_path = \"/path/to/server-chain.pem\"\n# private_key_path = \"/path/to/server-key.pem\"\n# client_ca_path = \"/path/to/client-ca.pem\"  # require mTLS client certs\n#\n# [http.mtls]\n# client_fingerprints = [\"sha256:<client-leaf-der-sha256>\"]\n#\n# Optional dedicated remote incident-response ingress. This is a second,\n# separately bounded listener; it is mandatory-mTLS and accepts only registered\n# certificates. The same fingerprint must also be allow-listed as an operator.\n# [http.control]\n# listen = \"0.0.0.0:7071\"\n# preauth_workers = 4\n# operator_workers = 1\n# doctor_workers = 1\n# The authenticated control probe bounds each request header and body to one\n# second to limit slowloris exposure. A slow request is closed and emits a\n# warn-level `reason=ingress_timeout` event; an unregistered certificate emits\n# a separate warn with its computed `mtls:sha256:...` fingerprint.\n#\n# [http.operator]\n# allow_loopback_owner = true\n# allowed_subjects = [\"mtls:sha256:<client-leaf-der-sha256>\"]\n\n# Signed audit chain. It records every privileged action in an append-only,\n# hash-chained, HMAC-SHA256-signed JSONL stream. Omit `path` to use\n# $XDG_STATE_HOME/oraclemcp/audit/audit.jsonl (or\n# $HOME/.local/state/oraclemcp/audit/audit.jsonl when XDG_STATE_HOME is unset).\n[audit]\npath = \"/var/lib/oraclemcp/audit/audit.jsonl\"\n# `key_ref` resolves through SecretResolver (env:, file:, or keyring:); its\n# resolved value must be at least 32 bytes of independently random material.\nkey_ref = \"env:ORACLEMCP_AUDIT_KEY\"\nkey_id = \"2026-q3\"\n# Retain old verification-only keys during rotation.\n# [[audit.verification_keys]]\n# key_id = \"2026-q2\"\n# key_ref = \"env:ORACLEMCP_AUDIT_KEY_2026_Q2\"\n\n# When no signed auditor exists because every reachable profile is READ_ONLY,\n# this separate redacted refusal/security-event floor is on by default at\n# $XDG_STATE_HOME/oraclemcp/corpus/refusals.jsonl. It is unsigned and not\n# tamper-evident; it never replaces the signed chain. Set false only to opt out.\nunsigned_refusal_log = true\n\n[[profiles]]\nname = \"dev_ro\"\ndescription = \"Read-only development database\"\nconnect_string = \"localhost:1521/FREEPDB1\"\nusername = \"APP_READONLY\"\ncredential_ref = \"env:ORACLE_APP_PASSWORD\"\nmax_level = \"READ_ONLY\"\ndefault_level = \"READ_ONLY\"\nrequire_signed_tools = true\ndashboard_ddl_workbench = false\n# Optional Oracle call timeout and request-budget ceiling. Omit for the 30s\n# default; set 0 only to opt out deliberately. Tool calls can tighten it with\n# timeout_seconds where advertised.\ncall_timeout_seconds = 30\n# Optional thin Session Data Unit request. Validated as 512..=65535 bytes.\nsdu = 32768\nlogin_statements = [\n  \"ALTER SESSION SET NLS_LANGUAGE = english\",\n  \"ALTER SESSION SET PLSQL_WARNINGS = 'ENABLE:ALL'\",\n]\n# Optional trusted local setup, authored by the profile owner and never by the\n# agent. Use for session-local initialization that is not an ALTER SESSION.\ntrusted_session_statements = [\n  \"BEGIN DBMS_OUTPUT.ENABLE(500000); END;\",\n]\n\n[profiles.oci]\n# Optional TCPS/wallet fields. Prefer these named fields over raw\n# connect_string query parameters when the value should be validated or redacted.\nwallet_location = \"/etc/oracle/wallet\"\nwallet_password_ref = \"env:WALLET_PASSWORD\"\nssl_server_dn_match = true\nssl_server_cert_dn = \"CN=dbhost.example.com\"\n# Optional SNI override. Omit for the driver default; set true only when the\n# endpoint's routing name is a rustls-valid DNS name.\n# use_sni = true\n\n# Optional proxy authentication. If enabled, `credential_ref` belongs to\n# `proxy_user`; omit top-level `username` or set it to the same value.\n# The database needs: ALTER USER <target_schema> GRANT CONNECT THROUGH <proxy_user>\n# [profiles.proxy_auth]\n# proxy_user = \"MCP_PROXY\"\n# target_schema = \"APP_OWNER\"\n\n# Optional DRCP server routing. Prefer these named fields over raw\n# connect_string query parameters so inheritance, validation, and redaction stay\n# predictable. This is separate from [profiles.pool], which controls local\n# client-side reuse.\n[profiles.drcp]\npooled = true\nconnection_class = \"ORACLE_MCP_AGENTS\"\npurity = \"reuse\"\n\n# Optional local client-side pool for stateless metadata/catalog reads where\n# pool-backed reads are used.\n# User SQL, LOB/sample reads, DBMS_OUTPUT, transactions, and session state stay\n# on the pinned main session. Served stateless HTTP uses bounded read-worker\n# lanes instead of sharing one pool across lane runtimes.\n# [profiles.pool]\n# max_size = 4\n# min_idle = 1\n# acquire_timeout_secs = 5\n# statement_cache_size = 50\n\n# Optional driver-level application context, applied during thin logon. Values\n# can carry tenant/session identifiers, so list_profiles and diagnostics redact\n# them. If inherited, setting entries here replaces the base list; omit to\n# inherit or set app_context = [] in the profile table to clear it.\n[[profiles.app_context]]\nnamespace = \"ORACLEMCP_CTX\"\nkey = \"tenant_id\"\nvalue = \"tenant-123\"\n\n[[profiles.app_context]]\nnamespace = \"ORACLEMCP_CTX\"\nkey = \"request_id\"\nvalue = \"req-456\"\n\n[profiles.session_identity]\n# Optional: all values are profile-local and are not shown by list_profiles.\n# oracle_connection_info reports these only as redacted field names.\n# Edition selection is applied during thin authentication before user SQL.\n# edition = \"ORA$BASE\"\nprogram = \"oraclemcp\"\nmachine = \"local-workstation\"\nos_user = \"local-operator\"\nterminal = \"agent\"\ndriver_name = \"oraclemcp\"\nmodule = \"oraclemcp\"\naction = \"inspect\"\nclient_identifier = \"agent\"\nclient_info = \"local-workstation\"\n```\n\nKeep the signed JSONL and its `<audit path>.anchor` sidecar together. Run\n`oraclemcp audit verify /var/lib/oraclemcp/audit/audit.jsonl` with the same\nsecret reference to re-walk the hashes, verify the MAC, and detect a truncated\ntail against that anchor. The unsigned refusal trail is deliberately outside\nthis command and cannot provide those tamper-evidence guarantees; it is the\ndiagnostic floor only while the signed tier is unavailable.\n\n`max_level` is the profile ceiling; `default_level` is the starting session\nlevel and must not exceed that ceiling. `call_timeout_seconds` defaults to 30\nseconds when omitted. It sets the Oracle driver call timeout for the physical\nconnection and the dispatcher request-budget ceiling for the whole tool call;\ntools that expose `timeout_seconds` can tighten that budget for one call but\ncannot loosen the profile ceiling. Set `call_timeout_seconds = 0` only as an\nexplicit opt-out from the driver call timeout; `doctor` warns on that posture.\n`login_statements` and `login_script` are for profile-local session policy only\nand are restricted to allowlisted `ALTER SESSION SET ...` parameters.\n`trusted_session_statements` are an explicit profile-owner escape hatch for\nlocal session initialization such as `DBMS_APPLICATION_INFO`, application\ncontexts, or `DBMS_OUTPUT`; they are never accepted from agent tool calls, and\nthey keep environment-specific conventions in private config rather than in the\nopen-source core.\n`session_release_statements` and `logoff_statements` are the matching teardown\nhooks for profile-owner cleanup: release hooks run only before a successful\npooled call returns its physical session to idle reuse, while failed or\ncancelled pooled calls are discarded; logoff hooks run immediately before\nlogical Oracle logoff.\nThe `oracle_connection_info` tool reports allow-listed connection posture\n(`backend`, connection strategy, server version, role/open mode, read-only\nstatus). Session identity and client topology fields such as `os_user`,\n`program`, `machine`, `terminal`, `client_driver`, `module`, `action`,\n`client_identifier`, and `client_info` are redacted by default and appear only\nas names in `redacted_fields` when present. The Rust thin backend can still set\nthe connect-time client identity fields (`program`, `machine`, `os_user`,\n`terminal`, and `driver_name`) from profile config, and it applies `module`,\n`action`, `client_identifier`, and `client_info` after connect through Oracle\nsession APIs.\n`require_signed_tools = true` requires HMAC signatures for operator-defined\ncustom tools on that profile; `protected = true` implies the same policy. The\nresolved audit, OAuth HS256, and custom-tool HMAC keys must each contain at\nleast 32 bytes of randomly generated key material.\n\nA few further profile keys are optional:\n\n- `base = \"other_profile\"`: inherit another profile's **unset** fields. A\n  child may override any inherited field, including raising `max_level` above\n  the base's value; `base` is configuration reuse, **not** a fleet safety\n  ceiling. To pin a production profile at `READ_ONLY`, set\n  `protected = true` (which requires `max_level = \"READ_ONLY\"`) on that child;\n  do not rely on a `READ_ONLY` base to constrain it.\n- `[profiles.pool]`: local client-side connection reuse settings\n  (`max_size`, `min_idle`, `acquire_timeout_secs`, `statement_cache_size`).\n  This enables the hybrid runtime strategy for stdio/direct dispatch and\n  lane-local metadata reads: catalog and metadata tools such as\n  schema/object/source inspection can use bounded stateless read connections,\n  while agent queries, sampled rows, LOB reads, DDL/write previews,\n  transactions, savepoints, temp tables, package globals, login setup, session\n  identity, and `DBMS_OUTPUT` stay on the pinned main session. Served stateless\n  HTTP routes generated metadata reads through bounded read-worker lanes instead\n  of sharing one pool across lane runtimes. When the stateless surface is live,\n  expect at least a pinned main Oracle session plus stateless pool session(s);\n  `oracle_connection_info` reports `connection_strategy = \"pinned_plus_stateless\"`\n  and the stateless pool details separately. `max_size` is the knob that caps\n  those additional stateless connections. `statement_cache_size` is passed to\n  the thin driver's bounded per-connection statement cache where pool-backed\n  reads are used; omit it to keep the driver default. This is separate from DRCP\n  server routing.\n- `[profiles.oci]`: OCI-specific connection settings for the underlying driver.\n  For TCPS/wallet connections, named fields are available for `wallet_location`,\n  `wallet_password_ref`, `ssl_server_dn_match`, `ssl_server_cert_dn`, and\n  `use_sni`. A wallet does not imply SNI; omit `use_sni` for the driver default\n  and opt in only when the endpoint's routing name is a rustls-valid DNS name.\n  Oracle's CPython driver can pass Oracle routing tokens through\n  `server_hostname`; rustls `ServerName` cannot, so host-as-SNI may skip that\n  one-negotiation routing fast path without breaking TCPS connectivity. Use the\n  named fields for values that should inherit through profiles, be redacted from\n  diagnostics, or be validated by strict config parsing.\n- `sdu = 32768`: optional thin driver Session Data Unit request size. Values are\n  validated as `512..=65535`; omit it to keep the driver's negotiated default.\n- `[profiles.drcp]`: Database Resident Connection Pooling server routing.\n  `pooled = true` appends `server=pooled`; `connection_class` maps to\n  `pool_connection_class`; `purity = \"reuse\" | \"new\"` maps to `pool_purity`.\n  Existing `connect_string` query parameters such as `wallet_location` are\n  preserved and DRCP parameters are appended with `&`. Prefer these named fields\n  over raw DRCP query parameters when the values should inherit, validate, and be\n  covered by redaction tests.\n- `[profiles.proxy_auth]`: thin proxy authentication. `proxy_user` is the\n  account that authenticates with `credential_ref`; `target_schema` is the\n  Oracle user granted `CONNECT THROUGH`. The connect `username`, if present,\n  must match `proxy_user`.\n- `[[profiles.app_context]]`: driver-level application context triples sent\n  during thin logon. Use typed `namespace` / `key` / `value` entries instead of\n  raw strings; values are treated as sensitive and omitted from ordinary profile\n  output. A child profile inherits the base list when omitted, replaces the whole\n  list when entries are set, and can clear inherited entries with\n  `app_context = []`.\n- `read_only_standby = true`: mark the target as a read-only standby so the\n  profile cannot be elevated above `READ_ONLY` regardless of `max_level`.\n- `mcp_exposed = false`: hide this profile from the MCP **agent-facing** surface\n  (E5). This is a **per-profile opt-out** — a profile is exposed to the agent\n  **by default**, and setting `false` hides only that one profile. A hidden\n  profile is invisible to `oracle_list_profiles`, `oracle_switch_profile`,\n  `oracle_search_objects`, and completion (a hidden or guessed name fails closed\n  identically); the operator/CLI (`oraclemcp profiles`, `doctor`, `--profile`)\n  still sees every profile. There is no global flip, and one profile's setting\n  never affects another's. It is a **visibility/scoping convenience, not an\n  access control** — the real bound on what a profile can do is\n  `max_level`/`protected`/DB privileges/the fail-closed classifier. At startup\n  the server logs a behavior-neutral exposure summary to stderr, e.g.\n  `MCP exposing 1 profile(s): dev_ro [ReadOnly] (1 hidden via mcp_exposed=false)`.\n  See [`docs/configuration.md`](docs/configuration.md) and the cross-profile\n  exposure threat in [`docs/threat-model.md`](docs/threat-model.md).\n\nThen launch:\n\n```sh\nexport ORACLE_APP_PASSWORD='...'\noraclemcp serve --allow-no-auth\n```\n\nConfig discovery order is:\n\n1. `$ORACLEMCP_CONFIG`\n2. `$XDG_CONFIG_HOME/oraclemcp/profiles.toml`, then `config.toml` (only when\n   `XDG_CONFIG_HOME` is set to an absolute path)\n3. `~/.config/oraclemcp/profiles.toml`, then `config.toml`\n\n`credential_ref` and `wallet_password_ref` resolve through the same\nSecretResolver seam as audit and HTTP secrets. Supported forms are `env:VAR`,\n`file:/path/to/secret`, `keyring:account` / `keyring:service/account`, and the\nfuture `vault:path` seam (fail-closed unless wired). `literal:value` is for\nlocal development only and is rejected when `protected = true`.\nFile-backed secrets are bounded to 64 KiB and must resolve without following a\nlink to a regular file. On Unix, the file must be owned by the service user,\nhave one hard link, and grant no permissions to group or other users.\n\nThe current `oraclemcp` thin adapter fails explicitly for auth/features it\ncannot serve end-to-end safely, such as external wallet auth without\nusername/password, autonomous OCI SDK/resource-principal token minting, and\nKerberos/RADIUS auth. These appear as structured unsupported diagnostics in\n`oraclemcp doctor --online --profile <profile>` and MCP error envelopes; the\nbinary does not silently fall back to thick mode.\n\n#### OCI IAM database-token auth\n\n`use_iam_token = true` under `[profiles.oci]` resolves a pre-fetched database\ntoken (a JWT) from exactly one source: `token_env`, `token_file`, `token_exec`,\nor the built-in `ORACLEMCP_IAM_TOKEN` when no explicit source is set. The token\nis injected through the thin driver's access-token connect path and is **refused\nover a non-TCPS transport** before it can reach the driver; `token_exec` is also\nrefused before spawn on non-TCPS. Token values are never persisted, rendered, or\nlogged. `iam_config_profile` still only parses and is reserved for a future\nautonomous OCI SDK/resource-principal token source. Real-ADB acceptance remains\nan operator smoke gate; autonomous OCI SDK minting/refresh remains deferred\n(bead k6q.9).\n\nThin result conversion materializes driver-side locators and cursors before\nserializing tool output: CLOB/BLOB/BFILE locators are read with the query LOB\ncaps, and valid REF CURSOR values or implicit result sets are returned as nested\nobjects containing child `columns`, `rows`, `row_count`, `fetched_count`, and\n`truncated` metadata. Nested cursor materialization has separate row, cell, byte,\nand depth caps, and unsupported shapes remain explicit instead of silently\nflattening or guessing. The same versioned `OracleCell.structured` payload is\nthe catalog-snapshot contract for the optional embedded `plsql-intelligence`\nengine: a catalog value is either preserved in a documented structured\nrepresentation or reported as a typed unsupported marker with provenance,\nnever as an ordinary-looking placeholder string. The separate `plsql-mcp`\nserver is deprecated.\n\nTo live-verify driver-level application context against Oracle 23ai/FREE, create\nan application context namespace in the test database, configure matching\n`[[profiles.app_context]]` triples, then query\n`SYS_CONTEXT('<namespace>', '<key>')` through `oracle_query` or run the optional\nlive test with `ORACLEMCP_TEST_APP_CONTEXT='namespace:key:value;namespace:key2:value2'`.\nInvalid or unauthorized context namespaces should fail at connect time with a\nstructured Oracle server error rather than falling back to post-connect SQL.\n\nTo live-verify edition selection against Oracle 23ai/FREE, create or reuse a\nvalid edition, set `[profiles.session_identity].edition`, connect with that\nprofile, and query `SYS_CONTEXT('USERENV','CURRENT_EDITION_NAME')` through\n`oracle_query` or `oracle_connection_info`. Invalid or unauthorized editions\nshould fail during connect/authentication with a structured Oracle server error;\noraclemcp must not silently fall back to the database default edition.\n\nProfile/config regression commands:\n\n```sh\n# Local, non-secret profile parsing/redaction/setup checks.\ncargo test -p oraclemcp-config -p oraclemcp-core profile -- --nocapture\ncargo test -p oraclemcp setup_payload_is_generic_and_client_ready -- --nocapture\ncargo test -p oraclemcp --test cli setup_write_round_trips_profiles_through_config_ops -- --nocapture\ncargo test -p oraclemcp profiles_json_reports_non_secret_metadata -- --nocapture\n\n# Live Oracle 23ai/FREE thin profile/config matrix.\n# Required: ORACLEMCP_TEST_DSN, ORACLEMCP_TEST_USER, ORACLEMCP_TEST_PASSWORD.\n# Optional: ORACLEMCP_TEST_WALLET_LOCATION, ORACLEMCP_TEST_WALLET_PASSWORD,\n# ORACLEMCP_TEST_SSL_SERVER_DN_MATCH, ORACLEMCP_TEST_SSL_SERVER_CERT_DN,\n# ORACLEMCP_TEST_USE_SNI, ORACLEMCP_TEST_PROXY_USER,\n# ORACLEMCP_TEST_PROXY_TARGET_SCHEMA, ORACLEMCP_TEST_EDITION,\n# ORACLEMCP_TEST_APP_CONTEXT, ORACLEMCP_TEST_DRCP=1,\n# ORACLEMCP_TEST_DRCP_CLASS.\ncargo test -p oraclemcp-db --features live-xe --test live_oracle -- --nocapture\n\n# Faster profile-only smoke subset.\ncargo test -p oraclemcp-db --features live-xe live_profile_config -- --nocapture\n\n# Heavy live load/soak (latency p50/p95/p99, leak/balance/drain). Additionally\n# opt-in via ORACLEMCP_LIVE_XE=1 on top of the same ORACLEMCP_TEST_* connection\n# env. See docs/performance-footprint.md.\nORACLEMCP_LIVE_XE=1 \\\n  ORACLEMCP_TEST_DSN=localhost:1521/FREEPDB1 \\\n  ORACLEMCP_TEST_USER=... ORACLEMCP_TEST_PASSWORD=... \\\n  cargo test -p oraclemcp-db --test load_soak -- --ignored --nocapture\n\n# Structured e2e harness and JSON-line logs for acceptance beads.\nbash scripts/e2e/run_all.sh --log --dry-run\nbash scripts/e2e/run_all.sh --log\n```\n\nStart a throwaway Oracle FREE 23ai database for the live suite with Docker (it\nprovides `FREEPDB1` on `:1521`):\n\n```sh\ndocker run -d --name oracle-free -p 1521:1521 \\\n  -e ORACLE_PASSWORD=<pw> gvenzl/oracle-free:23-slim\n```\n\nIf `serve --profile <name>` is provided, it overrides `default_profile`. If neither is set and exactly one profile exists, that sole profile is used.\n\nAgents can inspect available profiles with `oracle_list_profiles` and reconnect\nthe running MCP server with `oracle_switch_profile`. A failed switch leaves the\ncurrent connection in place.\n`oraclemcp serve --profile <name>` fails fast when the profile or config cannot\nbe resolved. Without an explicit profile, startup keeps discovery available even\nwhen the default live connection cannot be opened; live database calls then\nreturn structured tool errors instead of crashing the MCP server.\n\n### Operator-defined read-only tools\n\nOperators can expose environment-specific read helpers without forking the\nserver by placing TOML files in `~/.config/oraclemcp/tools.d/*.toml`. Set\n`ORACLEMCP_TOOLS_DIR` to use a different directory. Definitions are loaded and\nadvertised when `serve` starts, then revalidated before `oracle_switch_profile`\nreplaces the active connection; malformed files fail closed instead of silently\ndisappearing.\n\n```toml\n[[tool]]\nname = \"app_customer_lookup\"\ndescription = \"Lookup customer rows by id\"\nsql = \"SELECT id, name, status FROM app_customers WHERE id = :id\"\noutput_mode = \"rows\"\n\n[[tool.params]]\nname = \"id\"\ntype = \"integer\"\nrequired = true\ndescription = \"Customer id\"\n```\n\nCustom tool SQL uses named binds (`:id` above). Agent-supplied values are typed\nfrom `params` and bound by name; they are never interpolated into SQL text. The\nbinary loads definitions the classifier deems safe for the active profile\nceiling and binds them to the same runtime gate as operator-facing execution\ntools. Read-only tools execute on a dedicated read-only executor. Write/DDL/PL/SQL\ntools run through `oracle_execute`-equivalent semantics: statement-level\nclassification, session/profile gating, preview token verification when required,\nrollback-by-default for non-confirmed DML, and write-intent/audit recording on\ncommitted effects. Unproven package call definitions are rejected by gate policy\nas forbidden.\n\nOn protected profiles, every custom tool must carry a valid HMAC signature. Set\n`ORACLEMCP_CUSTOM_TOOLS_HMAC_KEY` in the server environment to verify signed\ndefinitions. On unprotected profiles, unsigned tools are allowed for local use;\nif any definition includes a `signature`, the same key is required and invalid\nsignatures are rejected. The key must contain at least 32 bytes of randomly\ngenerated key material.\n\nSign local tool definitions from the same binary:\n\n```sh\nexport ORACLEMCP_CUSTOM_TOOLS_HMAC_KEY='...'\noraclemcp sign-tool ~/.config/oraclemcp/tools.d/customer.toml --tool app_customer_lookup --write\n```\n\nBy default the command prints signature values to place into matching `[[tool]]`\nblocks; it does not print the HMAC key. Pass `--write` (alias `--in-place`) to\natomically place each generated signature in its matching `[[tool]]` block —\nincluding when the file ends with `[[tool.params]]`.\n\nCustom-tool signatures use the self-identifying\n`oraclemcp-custom-tool:v2:hmac-sha256:…` format. Version 2 authenticates every\nsemantic and agent-visible definition field, including nested parameter\ndescriptions, parameter order, and `output_mode`; only the `signature` envelope\nitself is excluded. Bare 64-hex signatures emitted by older releases are not\naccepted when present, and protected-profile startup fails with re-sign\nguidance instead of silently falling back to the incomplete legacy format.\nDuring upgrade, run `oraclemcp sign-tool` for each tools file and replace every\nlegacy `signature` value before restarting a protected profile.\n\n## Building and testing\n\n`oraclemcp` builds on a single **pinned Rust nightly** (`nightly-2026-05-11`,\nrecorded in `rust-toolchain.toml`). Two independent things need it: asupersync 0.3.9's\n`nightly-outcome-try` feature (`try_trait_v2` + `try_trait_v2_residual`), which\nis opt-in but on by default and reaches us via the `oraclemcp-driver-cx` 0.9.2 dependency; and,\non Windows only, `windows_by_handle`. The pin is **build-time only**: the\nshipped binary has no runtime dependency on nightly. See\n[`docs/toolchain.md`](docs/toolchain.md) for the full rationale and the\nre-pin runbook.\n\n```sh\nrustup toolchain install nightly-2026-05-11 --component rustfmt --component clippy\n\n# Build and run the full test matrix (the workspace's rust-toolchain.toml picks the pin).\ncargo build --workspace\ncargo test --workspace --all-targets\ncargo test --workspace --doc\n```\n\nBefore a release, the same gates CI enforces must pass on the pinned toolchain\n(full list and a copy-pasteable checklist in\n[`docs/release-checklist.md`](docs/release-checklist.md)):\n\n```sh\ncargo fmt --all -- --check                                  # formatting\ncargo clippy --workspace --all-targets -- -D warnings       # lint, warnings = errors\ncargo deny check                                            # advisories / licenses / bans / sources\nbash scripts/oraclemcp_agent_surface_lint.sh                # no arbitrary routine MCP surface\nbash scripts/oraclemcp_driver_seam_lint.sh                  # driver-adapter seam stays one file\nbash scripts/oraclemcp_honesty_grep.sh                      # no over-claiming framing\nbash scripts/oraclemcp_api_lock.sh                          # public API lock (no unreviewed surface drift)\nRELEASE_TAG=vX.Y.Z bash scripts/release_preflight.sh        # release metadata sync\n```\n\n### Running the live suite\n\nThe default test run needs no database. The live thin paths run against a real\nOracle 23ai via the unified `ORACLEMCP_TEST_*` env; a throwaway Oracle FREE is\nenough (provides `FREEPDB1` on `:1521`):\n\n```sh\ndocker run -d --name oracle-free -p 1521:1521 \\\n  -e ORACLE_PASSWORD=<pw> gvenzl/oracle-free:23-slim\n\nexport ORACLEMCP_TEST_DSN=localhost:1521/FREEPDB1\nexport ORACLEMCP_TEST_USER=... ORACLEMCP_TEST_PASSWORD=...\ncargo test -p oraclemcp-db --features live-xe --test live_oracle -- --nocapture\n\n# Heavy load/soak is additionally opt-in via ORACLEMCP_LIVE_XE=1.\nORACLEMCP_LIVE_XE=1 cargo test -p oraclemcp-db --test load_soak -- --ignored --nocapture\n```\n\nThe full live env block (wallet/TCPS, proxy, DRCP, edition, app-context vars) is\nin [the live test commands above](#connection-profiles) and in\n[`docs/operations.md`](docs/operations.md) §5.7. Connection-profile fields are\ndocumented in [`docs/configuration.md`](docs/configuration.md).\n\n## Supported Oracle versions\n\n| Dimension | Support |\n|---|---|\n| **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. |\n| **EZConnect** | Supported (`host:port/service`, plus EZConnect-Plus `tcps://…?wallet_location=…",
  "bytes": 60000,
  "sha": "1bc3051a453d724c1189ec342a2466e728660648baf8f4d88eb0fd42690cc16b",
  "repo_slug": "muhdur/oraclemcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_muhdur_oraclemcp_b8762ad6/readme"
}