Zademy

lu-query:やっと作り上げたOracle MCPサーバー

ツール
lu-query; Oracle; MCP; TypeScript
words 単語

2026 年 7 月 8 日、mcp-oracle-db という Java のリポジトリを作った。Oracle のデータベースをそのまま AI エージェントに渡して、自然言語で中身を見られるようにしたい。顧客側のスキーマを毎晩自分で追うのが面倒だった、というのが正直な理由だった。

できあがったのは pom.xml と README と、動かないクラスだけだった。そのまま四ヶ月、手放した。動くものを出すと言ったのに、出なかった。この空白は説明できない。空白は空白だ。

公開リポジトリとして実際に動き始めたのは 10 月 17 日で、作り直しを始めてから約 28 時間。11 リリース、156 コミット、8 回の publish。四ヶ月放置したものと、28 時間で出たもの。この落差は何なのか。

Java で始めて、空のリポジトリを残した

Java で始めた判断そのものが間違っていたわけではない。当時の Oracle 用ドライバも、MCP の書き方も、まだ事例が少なかった。

問題は規模だけだった。一つのツールに接続も、問い合わせも、説明も、編集も、全部押し込もうとした。動くものより先に設計図だけが完成して、そこで止まった。

あの四ヶ月間やっていたのは実装ではなかった。考え直していただけだ。考え直すだけなら、リポジトリを作らなくてもよかった。

28 時間で形になったもの

決めたのは Node 22 以上、TypeScript、ビルドは tsup、接続は oracledb 26 の thin モード。thin モードが効いた。Oracle Instant Client がいらない。Docker がいらない。LD_LIBRARY_PATH を触る必要もない。sqlnet.ora を書く必要もない。アカウントと接続文字列があれば動く。

毎台 Oracle のクライアントを整備する日々が終わった、それだけ価値がある。要件は Oracle 12c 以上。検証は Oracle Database 26ai Free 23.26.3 に対して行った。

代償もある。thin モードには autocommit も statement timeout もない。だから Promise.race で待たせて、breakExecution で強制中断している。ここはライブラリが助けてくれない部分だった。

規模はこう。TypeScript 7,009 行、35 ファイル。テストは 235 ファイル。実 Oracle に対する integration テストは 219 本。CI は 3 OS かける Node 22/24/26 の matrix。いまは 2.18.0、ライセンスは MIT。

三ヶ月かけて作った、とは言わない。公開したのは 10 月 17 日、作り直しは 28 時間。それ以前の空白とは別会計で見てもらいたい。

75 個の tool を持つということ

内訳は 1 システム、2 クエリ、3 DML、7 DDL、8 メタデータ中核、21 カタログ、7 オプティマイザ統計、2 PL/SQL、4 explain とチューニング、7 グラフ、V$ ビュー上のパフォーマンス系が 13。足して 75。

この分類自体に価値はない。ただカタログ系が 21 個あるのは効いた。エージェントは Oracle のスキーマを当てにいけない。USER_TABLES も USER_TAB_COLUMNS も USER_CONSTRAINTS も、推測ではなく引ける。「どのテーブルのどの列か、まだ分からない」という時間を削れる。

V$ の 13 個も効いている。list_blocked_sessions で塞がっているセッションが分かり、get_wait_events で待っている理由が分かり、list_top_sql で重い SQL が見つかる。推測ではなく観測だ。

一方で 75 個は多い。接続先のデータベースが小さいとき、全部並べて選んでもらうのは効率ではない。必要なものだけ段階的に見せる仕組みは、次の課題として残っている。

SQL は一箇所を通す

すべての SQL は DataAccess(src/db/data-access.ts)を通る。そこに制御を寄せている。statement timeout は 120 秒、ソケットの読み書きは 30 秒、返す行数は 1000 行まで、commit は明示的。

これは後から足すための仕組みではない。エージェントから見たときの境界を、コードの構造そのもので強制するためだ。どこか別の場所で execute を握りつぶして直接流す経路を作らない。すべての tool path がここを通る。

この一点集中は、安全のためだけに置いたわけではない。あとから「この SQL はなぜそうなったのか」がたどれるようにするためでもある。

「read-only」は受け皿にならない

SQL MCP 全体への信頼が下がっている。Anthropic の Postgres 参照実装は 2025 年 7 月 10 日にアーカイブされた。read-only モードを回避する SQL injection があったためだ。executeautomation の mcp-database-server にも未修正のバイパスが残っている。そして 2026 年 4 月の研究では、read-only を謳った SQL MCP サーバー 17 個すべてが突破されたという報告がある。

Oracle 固有の話ではない。それでも「read-only だから、このエージェントに預ければ大丈夫」という理屈は、もう成立しない。権限の話であって、型チェックの話ではない。

そこで lu-query は allow-list 方式で、SQL の種類ごとにツールを分けている。問い合わせは SELECT のみ。書き込みは別のツールで、プレビュー、ロールバック、実行、と段階を踏ませる。接続情報は環境変数から取る。

これはファイアウォールではない。守ると主張するほど大げさでもない。ただ、被害を受ける面積は確実に減る。監査ログには生の SQL が残る。42 個の Markdown ファイルに、session ごとに書き出していく。JSON で殴り合うような形式ではない。

余談だが、Oracle 自身が公式の Database Tools 用 MCP サーバーを出している。OAuth 2.0 と OCI IAM、Streamable HTTP、NL2SQL まで揃っていて、Autonomous Database では adb$feature を使う形になる。かなり作り込まれている。こっちのは試作にすぎない。

テストは mock ではなくプロトコルで

FakeDataAccess と InMemoryTransport を使った。サーバーを起動させず、tools/list と tools/call を直接叩く。リクエストの形そのものを検証する。

ここでいちばん大きかったのは、テストが緑なのに製品が 100 パーセント死んでいたことだった。

CURRENT_SCHEMA のキャッシュ取得だった。Oracle の USER は大文字で返る。CURRENT_SCHEMA も大文字。fake は小文字を返していた。結果、カタログ系 21 個のツールが全部空を返すという惨状だった。

テストは緑だった。他の integration テストも緑だった。全部、嘘をついていた。製品側のバグを直しただけでは緑に戻らなかった。fake 側も直さないと、2.16.3 でやっと緑に戻った。

二つ目は LOB の循環参照だった。CLOB、NCLOB、BLOB は connection を握っていて、JSON.stringify が「Converting circular structure to JSON」で落ちる。同じリリースで直した。

構文解析も既存パーサーを信用できなかった。node-sql-parser に Oracle の方言がない。MERGE は UNKNOWN として返る。だからツール側で判定するしかなかった。ツールが増えた部分を隠すこともできなかった。

ベンチマーク用の skill を足したとき、増えたツールは 0 だった。既存の組み合わせだけで前後を取る。75 個の壁は、超えずに済んだ。

気に入っている点

  • 75 tools、4 resources、9 prompts。プロトコルの実装が DB の都合に振り回されない。
  • SQL が全部 DataAccess を通る。抜け道を意図的に作らない。
  • 監査ログが session 単位の Markdown。あとから人間が読める。
  • ベンチマーク skill がツールを増やさない。数に迷わずに済む。
  • npm は OIDC の trusted publishing を使っていて、長期トークンを手元に置かなくてよい。

気になる点

  • 認証がない。stdio と localhost を前提にしている。エージェント自身が SQL を書く前提で動いている。
  • マルチテナント隔離はない。テナント境界をどこで切るかは、まだ決めていない。
  • NL2SQL はない。意図的に入れていない。
  • 外部からの contribution は 0。star は 2、初日のダウンロードは 725。誇れる数字でも、際限った数字でもない。
  • CHANGELOG.md は 2.16.3 まで。package.json は 2.18.0 と言っている。追いついていない。
  • topics に sqlite が残っている。Oracle しか扱わないプロジェクトのはずなのに、コピーした痕跡。

今のところの結論

28 時間で出たものと四ヶ月の空白の差分は、実のところ thin モードのドライバがあったからだと思う。あのまま Java で続けていたら、Instant Client の設定で時間を溶かしていた可能性が高い。ここは推測で、確かではない。

ただ正直に言えば、このプロジェクトが返してきたのは答えではなく、疑問のほうが多い。75 個のツールのうち、どれを信じ、どれに迷い、どれをまだ理解していないのか。ドキュメントを読んでいて、その三つに分けていく自分がいた。

それでも、その 28 時間は無駄ではなかった。問いが増えるのは、やったという証拠だと思う。

参考リンク


次の動画では、「read-only は信用できない」の話を扱いたい。突破された SQL MCP の事例をいくつか並べて、実際にどこで壊れるのかを確かめる。33 秒の紹介動画もリポジトリの docs/lu-query-promo.mp4 に入れてあるので、先に見てもらえる。

では、また次の動画で。