Zademy

lu-query: the Oracle MCP server I finally finished

Tools
lu-query; Oracle; MCP; TypeScript
words words

There is a repository on my GitHub profile called mcp-oracle-db. I created it on July 8th. It has no commits. It has nothing in it at all, not even a README I can remember writing. Every couple of weeks I would open it, stare at the empty tree, and close the tab again.

That was the first attempt at the idea I just finished shipping: an MCP server that puts an Oracle database in front of an AI agent. I started it in Java. Then the week went sideways, then another project showed up, and the idea went into the drawer where unfinished things go.

Two days ago I finished it. Not a prototype. It runs, it's on npm, it has tests that talk to a real Oracle. This is what I learned writing it, and also what I still don't think is good enough.

The same idea, in a different language

I'm not going to pretend the Java version was a near miss. It was empty.

My honest suspicion is that it got heavy in the wrong order. Before I had a single tool that answered a real question, I was already thinking about the driver, the client, how to structure the protocol. Configuration instead of output.

The rewrite started from the other end. One tool that did something I actually needed, then the guardrails around it, then the breadth.

Version 2.18.0 now. MIT. 11 releases, 156 commits, 8 publishes in about 28 hours, which is a pace I do not recommend. Node 22 or newer, TypeScript, oracledb 26 in thin mode, built with tsup.

The part I'm most proud of is architectural and boring: every piece of SQL goes through one file. DataAccess in src/db/data-access.ts. Query timeout, the row cap, the audit write, the explicit commit. If that file is wrong, everything is wrong, and at least I know where to look.

The boring parts turned out to be the whole job

Thin mode is the single biggest reason anyone will actually try this. No Oracle Instant Client. No Docker. No LD_LIBRARY_PATH. No sqlnet.ora. You point it at a database and it connects. The usual Oracle driver setup is where I got stuck the first time, and it's a lot of ceremony for someone who just wants to ask a question of a table.

It requires Oracle 12c or newer. I test against Oracle Database 26ai Free 23.26.3.

The defaults: a pool of 4 connections, a 120 second statement timeout, a 30 second socket timeout, and a 150 second MCP watchdog on top. Reads are capped at 1000 rows server side, and pagination goes through run_query(limit, offset). Not because 1000 is a magic number, but because an agent asking for "all the rows" should hit a wall early rather than melt your temp tablespace.

Then the guardrails. Identifiers have to match ^[A-Za-z][A-Za-z0-9_$#]{0,127}$ or they don't run, and literals get escaped. run_query accepts SELECT and nothing else. Writes go through separate tools that force preview, then rollback, then execute. Credentials come from environment variables and are never written to disk or to logs.

That last set of decisions is not decorative. Anthropic archived their own reference Postgres MCP server in July 2025 after a SQL injection that evaded read-only mode. There's a community server with an unpatched bypass. An April 2026 study got into 17 out of 17 read-only SQL MCP servers, all of them.

I can't tell you mine is immune. I can tell you the failures were structural, and that the structural answer is an allow-list, escaped literals, and DML on different tools than reads.

But let me be clear about what it is not. It's not a firewall. The permissions of the database user you connect as are the first real barrier, and they were always there. And the audit logs store the literal SQL, which means they are an attack surface too, not just a receipt.

The two bugs that made me distrust my own tests

Both of these shipped. That part still bothers me.

The first one was CURRENT_SCHEMA. The driver returns that column in uppercase. My per-connection cache expected something else, so it never populated, so every catalog tool failed. The database was fine. The whole catalog half of the server was dead.

And the test suite was green. Because the fakes returned lowercase. My fake and the real driver disagreed, and the tests were cheerfully testing the disagreement.

I fixed the bug and I fixed the fake. Version 2.16.3.

If you take one thing from this: your fake is not a stub, it's a second implementation. The moment it disagrees with the driver, you are not testing your code, you are testing your assumptions about it.

The second was LOBs. CLOB, NCLOB, BLOB came back holding a reference to the connection, and JSON.stringify died with "Converting circular structure to JSON". Every table with a CLOB in it was unusable. Also 2.16.3.

7,009 lines of TypeScript across 35 files, 235 test files, 219 integration tests against a real Oracle, CI across three operating systems and Node 22, 24 and 26. That is a lot of machinery, and both of those bugs still got through it.

Where TypeScript ran out

Three places, and I want to be honest about them instead of papering over it.

node-sql-parser has no Oracle dialect. It parses Oracle as PostgreSQL, and MERGE comes out as UNKNOWN.

oracledb in thin mode does not autocommit and has no stmtTimeout. I simulate it with Promise.race plus breakExecution. It works, but it is a simulation and I'd rather you knew that.

EXPLAIN PLAN needs PRAGMA AUTONOMOUS_TRANSACTION blocks so the plan query doesn't contaminate the session it's inspecting.

In all three cases my answer was the same: write down the gap. Don't fake the missing piece and hope nobody looks.

What I like

The benchmark skill added exactly zero new tools. I went in expecting to write one, then verified piece by piece against the real database that it was already composable with what existed. Shipping nothing is a feature.

The audit trail is Markdown, one file per session, not JSON. There are 42 real ones sitting in mcp-audit-logs/ and I can read all of them with grep. A human can read them.

The HTML reports live inside skills/, not in dist. That way a dependency mistake in a report template can never break the server build.

Publishing to npm uses OIDC trusted publishing. No token, no OTP, nothing to leak.

And the 75 tools break down like this: 1 system, 2 query, 3 DML, 7 DDL, 8 core metadata, 21 catalog, 7 optimizer stats, 2 PL/SQL, 4 explain and tuning, 7 graph, and 13 performance tools working on V$ views.

What worries me

725 downloads on the first day. Two stars. Zero external contributions. I'm not going to put a nice frame around that.

Oracle already ships an official MCP server. Database Tools, with OAuth 2.0 and OCI IAM, Streamable HTTP, NL2SQL; Autonomous exposes its own MCP endpoint behind the adb$feature tag. That one has authentication. Mine does not. Mine has no multi-tenant story and no NL2SQL either.

What mine has is that it's local, needs no cloud account, no IAM, no HTTP surface, and it audits every statement. That's the whole pitch. For a lot of people that will not be enough, and for most of the Oracle shops I know it definitely won't be.

235 test files for 35 files of source. I don't think that ratio is healthy. A lot of it is good discipline and some of it is me not wanting to look at data-access.ts again.

The CHANGELOG.md only reaches 2.16.3 while package.json says 2.18.0. There are also leftover sqlite topics on a package that only talks to Oracle. Trivial stuff, but it's exactly the fingerprint of shipping faster than I tidy.

And 75 tools is a lot of surface area. Every one of them is another place for a guardrail to be wrong.

Where it stands

For me the win was never the 75 tools. It was that the agent stopped guessing my schema because there are 21 catalog tools to ask. It's that when something is slow there are 13 tools reading V$ views, like list_blocked_sessions or list_top_sql. It's that I can diff before and after with DBMS_XPLAN and I have a per-statement record of what actually ran. That's the daily work.

I won't give you a benchmark, because I don't have one of my own. What looks like measurement in the benchmark skill's documentation is illustrative, not real. So treat speed claims skeptically. Mine too, if I start making them.

Anyway: nobody else has written a line of this yet. It is mostly a tool I built for my own problem. If you have the same problem, it's on npm and it's MIT. If you don't, read the repo and take whatever parts are useful.

Resources

See you in the next video.