📘

AIエージェント向けのツールを、MCPではなくCLIとして作った — 設計の3原則

に公開
2

AI エージェントのためのデータ分析環境 Codatum CLI(cdm)をベータ版としてリリースしました。この CLI は、人間がターミナルから使うだけでなく、Cursor や Claude Code のような AI エージェントに操作させることを主要な用途のひとつとして設計しています。

https://codatum.jp/cli
https://codatum.jp/blog/post/codatum-cli-beta
https://cli-docs.codatum.com/

前提として、Codatum は SQL 駆動のノートブックでデータウェアハウスに直接接続して SQL を実行し、その結果を分析・可視化・共有するためのデータ分析プラットフォームです。cdm は、この Codatum のノートブックをローカルのファイルとして扱い、編集・検証・更新を CLI から行えるようにするものです。

エージェント連携というと今は「MCP サーバーを立てる」がほぼ既定の選択肢になっていますが、cdm は最終的に MCP サーバーではなく、CLI として公開する形になりました。

この記事では、なぜそうなったのか、そして「AI エージェントに操作させる CLI」を作るうえで何を考慮したのかを、実装ベースでまとめます。MCP を否定する話ではなく、「自分たちの要件では CLI で実装するのが噛み合った」という一例として読んでもらえればと思います。

そもそもの経緯:MCP を試していたら CLI に行き着いた

最初から CLI と決めていたわけではありません。むしろ逆で、ローカルの MCP サーバを立てるための CLI を作って検証していく過程で、純粋な CLI として提供する方が良いと判断するに至りました。

冒頭で触れたとおり、Codatum には元々 SQL 駆動のノートブック機能があります。これを AI エージェントに触らせたい、というのが出発点でした。

エージェントに触らせるには、まずノートブックをエージェントが読み書きできる形式にする必要があります。そこで、ノートブックをマークダウン形式のファイルとして表現する方式を検討し始めました。ただ、こういうフォーマットは机上で「良さそう」と思っても、実際にエージェントに編集させてみないと本当に扱いやすいかは分かりません。

そこで、エージェントにノートブックを操作させる仕組みも並行して作り始めました。当初は、MCP サーバーをメインの連携方式として実装しようとしていました。ノートブックの編集・検証・更新といった操作を MCP のツールとして提供する、という構想です。

ところが、MCP 経由でマークダウンファイルの検証機能を試していたときに違和感を覚えました。エージェントは、ローカルのファイルを読み込み、その内容を MCP ツールに渡すためのリクエストボディを組み立て、ツールを呼ぶ、という一連の処理をすべて自分の「頭の中」=コンテキスト上でこなそうとします。やりたいことは「このファイルを検証する」だけなのに、そのためのお膳立てにトークンを消費している。CLIで直接ファイルを検証すれば、cdm notebook validate <file> の一発で済む話です。

MCP のツールとローカルファイルの相性がそもそも良くない。この非効率に気づいたことをきっかけに、MCP をメインに据える方針をやめ、CLI を主軸に据えて機能を伸ばしていく方向に切り替えました。その結果が、今の cdm です。

CLI の強み:ファイルを中間表現にできる

なぜ CLI だと素直なのか。その核心は、ファイルを中間表現として扱えることにあります。

MCP でファイルを扱おうとすると、構造的な無理が生じます。MCP のツールは基本的に「引数を受け取ってリクエストを処理する」という流れなので、ノートブックのような構造化されたファイルを扱わせるには、エージェントが以下を肩代わりすることになります。

  1. ファイルの内容を読む
  2. パースして、ツールに渡すリクエストボディを組み立てる
  3. ツールを呼ぶ
  4. 返ってきた結果を解釈して、必要ならファイルに書き戻す

先ほどの「エージェントがリクエストボディの組み立てをコンテキスト上でこなしていた」場面は、まさにこの 2 が表面化したものです。この過程はすべてエージェントのコンテキスト上で行われるため、ファイルが大きいほどトークンを消費し、しかも組み立てを間違える余地も増えます。

ここで効いてくるのが、MCP のツールがファイルを値渡しでしか受け取れない点です。ローカルの 1MB のノートブックを検証させたいだけでも、その中身をツールの引数に詰める以上、1MB 分のテキストが一度エージェントのコンテキストを通過します。厳密には、ローカル MCP サーバーを立ててファイルパスを渡せば、サーバー側でファイルを参照させることもできます。ただ、ローカルでもリモートでも同じように使えることが売りの MCP で、あえてファイルパスをやり取りする設計にするくらいであれば、CLI にファイルを渡すほうが直感的と考えました。

CLI では、この問題が構造的に消えます。ノートブックのファイルそのものを引数として扱えるからです。エージェントの役割は「ファイルを直接編集する」ことだけで、編集・検証・サーバーへの同期といった処理は cdm 側が引き受けます。

# エージェントはファイルを直接編集し、あとはコマンドを1行叩くだけ
cdm notebook validate ./notebooks/sales
cdm notebook format ./notebooks/sales
cdm notebook push ./notebooks/sales

この「ファイルを引数として渡せる」ことが、以降で述べる設計すべての土台になっています。MCP だとファイルを引数として直接渡せないため、多くの処理をエージェントの「頭の中」=コンテキスト上でやらせることになります。

CLI の強み:人間・CI・エージェントが同じインターフェースに乗れる

もうひとつの強みは、CLI が特定の使い手に依存しないことです。MCP はあくまで LLM エージェントがツールを呼ぶためのプロトコルなので、CI のような「エージェントが介在しない自動化」とは噛み合いません。cdm notebook validate を CI で回すために MCP サーバーを立てて……とはなりません。CLI なら、人間がターミナルから叩くのも、CI が叩くのも、エージェントが叩くのも同じコマンドで、同じドキュメントを元に操作できます。

以下では、この CLI を作るなかで見えてきた「エージェントに操作させる CLI」の設計原則を 3 つに整理します。3 つに共通しているのは、エージェントは間違えるし、コンテキストは有限だという前提に立って、ツール側でどれだけ引き受けられるかという観点です。

原則1:コンテキストを浪費させない

エージェント向けツールを設計するうえで最初に効いてくるのが、コンテキスト(トークン)をどう使わせるかです。エージェントが使えるコンテキストは有限で、ツールの仕様説明、扱うファイルの中身、コマンドの出力で簡単に埋まっていきます。コンテキストを本題の判断に使わせるために、ツール側で節約できるところは節約する。そして、モデルが事前学習で既に持っている知識に乗っかって、教え込む量そのものを減らす。この 2 点を重視しました。

ファイル形式は、モデルの事前学習を活かせるものを選ぶ

ファイルを引数に使うと決めたら、次はそのファイルをどんな形式にするか、です。ここでも「エージェントにとってのコスト」が判断基準になります。

ノートブックの内部構造を厳密に表現するだけなら、JSON や YAML も候補でした。構造化データとしては素直ですし、スキーマも定義しやすい。しかし、エージェントに書かせることを考えると効率が良くありません。JSON や YAML は、括弧やインデント、キーの繰り返しといった構造を保つための要素そのものにトークンを消費します。ネストが深くなるほどこのオーバーヘッドは効いてきますし、閉じ括弧の対応やインデントのズレで parse エラーも起きやすくなります。

その点、マークダウンは構造のための記号が少ないため、トークン効率が高く、壊れにくい。文章もコードブロックも自然に書けて、エージェントにとって扱いやすい形式です。厳密な内部構造の再現性より、エージェントが効率的に・正確に書けることを重視して、ノートブックはマークダウンを拡張した形式にしました。

さらに大きいのが、コーディングエージェントとしての事前学習です。近年のモデルは、どれもコードやファイルを読み書きするための訓練が相当に積まれています。「ファイルを開いて、一部を書き換えて、保存する」「コマンドを実行して、出力を見て次の手を決める」といった、コーディングの現場で当たり前の操作は、モデルがすでに得意とするところです。ファイルを直接編集させる cdm の設計は、この蓄積を活用できます。

この観点は、ファイル形式だけでなくコマンド体系の設計にも効きますvalidate(検証する)、format(整形する)といったコマンド名は、コーディングの世界にすでにある概念です。エージェントはこれらの単語が何を意味し、開発のワークフローの中でどう使うものかを、事前学習を通じて知っています。だから「編集したら validate して、format で整える」という流れを、こちらが細かく説明しなくても素直に受け入れます。ゼロから独自の操作体系を教え込むのに比べて、圧倒的に効率的です。

同じ発想は、マークダウンを拡張する際の細部にも効かせています。独自の構文をゼロから発明するのではなく、既に存在する記法を活用するということです。たとえば SQL 本文と実行のためのメタデータを定義する SQL ブロックの表現には :::sql-block のようなコンテナディレクティブ(マークダウンの拡張記法として実在するもの)を、パラメータ参照には $1 というプレースホルダ(多くの言語・ツールで見慣れた形式)を使っています。

独自構文や独自コマンドを発明すると、その使い方をプロンプトやドキュメントで一から教え込む必要があり、エージェントも慣れないものほど間違えやすくなります。既にある概念に乗れば、教える量が減り(=プロンプトが薄くなり)、エージェントは事前学習の知識で正確に動けます。モデルが事前学習で何を身につけているかを見極めて、それを活用することを重視しました。

ドキュメントは「全部積む」のではなく「必要なときに取りに行かせる」

もうひとつのコンテキストの浪費源が、ツールの仕様やドキュメントの扱いです。

エージェントに複雑なツールを正しく使わせようとすると、つい「仕様を全部システムプロンプトに書いておこう」となりがちです。しかし、これはコンテキストを固定的に浪費します。ノートブックのフォーマット仕様、チャートの種類ごとの設定、SQL ブロックの参照の仕組み……といった大量のドキュメントを常に渡すのは現実的ではありません。

cdm では、ドキュメント自体を CLI から取得できるようにしました。

cdm doc index          # ドキュメントの目次(llms.txt 形式)を取得
cdm doc show <path>    # 個別ページの本文を取得
cdm doc full           # 全ドキュメントを1ファイルで取得(grep 用途など)

狙いは、必要なものを必要なタイミングでエージェント自身に取りに行かせることです。最初に目次(doc index)だけを渡しておき、チャートを作るタイミングでそのチャートのドキュメントを doc show で取りに行かせることで、プロンプトを肥大化させず、そのタスクに必要な情報だけをコンテキストに載せられます。

この「取りに行かせる」設計は、次に述べる原則2(間違いからの復帰)とも噛み合います。エラーメッセージがドキュメントの URL を返し、エージェントがそれを doc show で引いて自己修正する、というループが組めるからです。

原則2:間違えさせない努力より、気づいて直せる設計に投資する

エージェントが間違えるのは当たり前です。問題は、それにどう対処するか。ここに発想の転換があります。

間違いを減らそうとすると、ついプロンプトに禁止事項を積み上げる方向に走りがちです。「〜してはいけない」「〜する前に必ず〜せよ」を、失敗を見つけるたびに書き足していく。私も過去にそういうプロンプトを育てては、メンテナンスで苦しんだ覚えがあります。

これは、LLM が出はじめた頃の癖のように思います。当時はモデルの能力が心もとなく、細かく指示しないと期待どおりに動いてくれない、と感じることが多くありました。そのため、あれもダメこれもダメと行動を細かく縛るプロンプトを書きがちでした。しかし、今のモデルは十分に賢く、状況と目的を与えれば自律的に判断して動けます。だとすれば、事前に細かく指示するより、エージェントが自律的に動ける前提で、判断に必要な仕組みと情報を提供するほうが理にかなっています。禁止事項は際限なく増え、増えるほど互いに矛盾し、コンテキストが浪費されます(原則1)。プロンプトは薄く保つほど良いのです。

なので cdm では、間違えさせない方向には投資せず、間違えた結果にエージェント自身が気づけて、そこから直せる方向に投資しました。エージェントの出力を検証して具体的なフィードバックを返し、機械的に直せるものは機械的に直す。ツール側に「気づかせる仕組み」と「修正しやすい仕組み」を持たせる、という発想です。

気づく手段(1):構造的な誤りは検証で捕まえる

まず、構文・スキーマレベルの誤りを validate で捕まえます。自動修復できないエラーについては、エージェントが自力で直せるように、エラーメッセージの情報量を上げています。

実際の cdm notebook validate の出力はこうなっています(一部を抜粋・調整しています)。

$ cdm notebook validate ./
./sales-dashboard: Validation error: Invalid ObjectId at "id" (L74:1-L74:14)
  72 |
  73 | ```yaml {.chart}
  74 | id: chart_001
  75 | sqlId: 69951b2bb5ae777543404e8a
  76 | chartSettings:
Documentation:
  - https://cli-docs.codatum.com/md/doc-page#chart-block
  - https://cli-docs.codatum.com/md/appendix#common-types

このメッセージには、エージェントの修正に効く要素が 3 つ入っています。

  1. エラー箇所が行番号と該当行の抜粋で示されるL74:1-L74:14 と前後のコード)。エージェントはファイル全体を読み直さなくても、どこを直せばいいか即座に分かります。これはトークンの節約にもなります。
  2. エラーが具体的で一意Invalid ObjectId at "id")。id: chart_001 は ObjectId 形式ではない、という具体的な指摘なので、何をどう直すかが明確です。
  3. 直し方が載っているドキュメントの URL を渡す。エラーメッセージが「何が悪いか」だけでなく「どう直すか」への導線を持っています。エージェントはこの URL を cdm doc show で引いて、正しい形式を確認できます。

重複 ID のように、離れた 2 箇所が絡むエラーでも、両方の位置と抜粋を返します。

./monthly-report: Duplicate sql-block.id: 699528138c94f5cd9eb1de7b in the same page (L42:1-L57:4, L946:1-L957:4)
  41 | :::sql-block
  42 | ```yaml {.attrs}
  43 | id: 699528138c94f5cd9eb1de7b
  ...
 945 | :::sql-block
 946 | ```yaml {.attrs}
 947 | id: 699528138c94f5cd9eb1de7b
  ...
Documentation: https://cli-docs.codatum.com/md/sql-block#attrs

「同じページ内で ID が重複している」「その 2 箇所はこことここだ」まで示されれば、エージェントは片方を張り替えるだけで直せます。

前提:ドキュメントをインターフェースとして磨く

この自己修正ループが成立する前提として、参照先のドキュメントが正確であることが必要不可欠です。エラーメッセージが URL を返し、エージェントがそれを読んで自分を直すのですから、ドキュメントは実行の経路上にあります。エージェントに使わせるツールでは、ドキュメントは単なる付属物ではなく、コマンドと並ぶインターフェースの一部です。だからこそ、正確で、矛盾がなく、分かりやすいことが決定的に重要です。曖昧だったり実装と食い違っていたりすると、エージェントはそれを頼りに間違った方向へ修正してしまいます。

cdm の開発では、実際に仕様の検証とドキュメントの作成に一番時間を割きました。ドキュメントを先に書いて精査し、CLI の実装はそのドキュメントを元に組み立てる、という進め方をした部分もかなりあります。エージェント向けのツールでは、この「ドキュメントをインターフェースとして磨く」工程が、そのままツールの品質を左右します。

気づく手段(2):見た目の誤りは画像にして捕まえる

validate が捕まえられるのは、構造として壊れている誤りです。しかし、構文的には正しいのに、見た目が破綻しているケースがあります。チャートの軸ラベルが重なって読めない、凡例がはみ出す、ダッシュボードのレイアウトが崩れる、といった類です。これらは構文上は正しいので、検証では捕まえられません。

そこで cdm では、ノートブックのページやチャートを画像(PNG)として書き出せるようにしています。

cdm notebook render page ./notebooks/sales.cnb.md    # ページを画像として出力
cdm notebook render chart ./notebooks/sales.cnb.md -k pageId:xxx/chartId:yyy

狙いは、書き出した画像をエージェント自身で目視で確認させることです。マルチモーダルなエージェントであれば、ラベルの重なりやレイアウトの崩れを画像から判断し、チャートの設定を修正できます。ここでも「エージェントが最初から完璧なチャートを設定する」ことは期待せず、「出力を見て気づいて直す」ループに乗せます。

直す手段:機械的に直せるものは、ツールが直す

誤りのうち、機械的に直せるものは、そもそもエージェントに直させずにツール側で直せるようにしました。ノートブックファイルの編集では、validate(検証のみ)と format(検証+整形+自動修復)を分けています。

分かりやすい例が、ノートブック内の要素に振る ID です。ページや SQL ブロックには一意な ID(Mongo の ObjectId 形式のランダムな文字列)が必要ですが、LLM はランダムな文字列を生成するのが得意ではありません。実際にエージェントにノートブックを書かせてみると、ID の生成まわりで苦戦する場面が目立ちました。重複した ID を振ってしまったり、形式の合わない文字列を生成してしまっていました。

そこで、ID はエージェントに振らせるのをやめ、ツール側で機械的に付番する方向に寄せました。エージェントは ID を空のまま書いてよく、format が正しい形式の ID を自動付与します。

cdm notebook format ./notebooks/sales.cnb.md

エージェントに「一意なランダム ID を正しく振ってから書け」と要求するのではなく、「ID は空でいいから書いてくれれば、こちらで振る」という分担にしたわけです。苦手なことを無理にやらせず、コードで確実にできることはツール側の機能として持たせるようにしました。

一方で、エージェント自身が別の場所から参照するために、書いている最中に ID を確定させたいケースもあります。こうした ID は、cdm util gen-id で正しい形式のものを生成させる導線を作っています。これも「ランダム文字列は LLM に作らせず、コマンドに作らせる」という同じ発想です。

cdm util gen-id        # ObjectId 形式の ID を生成(ネットワーク・認証不要)

「コードで確実にできることはツールが引き受け、LLM が苦手なことは無理にやらせない」という考え方です。

原則3:ワークフロー全体を、シンプルな一本道に設計する

エージェントに複雑な手順を踏ませると、「順番を間違える」「途中で別のことを始める」「中断する」といった迷走の可能性が増えます。逆に言えば、エージェントがたどる作業の流れ全体を、できるだけ分岐の少ない一本道に設計することが、そのまま品質の向上につながります。

この「一本道に設計する」には、3 つのレイヤーがあります。個々の操作を単純にする(1 コマンドへの畳み込み)、操作の繰り返しを単純にする(単純なループ)、そして流れ全体をエージェントに正しく伝える(ドキュメント)の 3 レイヤーです。

個々の操作を単純にする:多段の手順は1コマンドに畳み込む

分かりやすいのが SQL の実行です。Codatum では、ノートブック内の SQL は非同期ジョブとして実行され、本来は「ジョブを作成する → 完了をポーリングする → 結果を取得する」という多段の手順が必要です。

これを MCP のツール呼び出し単位で表現すると、「クエリの実行を開始する」→「実行が完了したか状態を確認する(完了するまで何度も繰り返す)」→「完了したら結果を取得する」という往復を、エージェント自身に管理させることになります。とくに真ん中の「完了するまで状態確認を繰り返す」ポーリングのループをエージェントの判断に委ねるのは、待ちきれずに先へ進んでしまったり、いつまでも確認し続けたりと、事故のもとです。

cdm では、ノートブック全体の実行を 1 コマンドに畳み込みました。

cdm notebook run ./notebooks/sales.cnb.md

このコマンドは、内部でノートブック内の各 SQL を順に実行し、キャッシュの判定(変更のない SQL は再実行しない)、ジョブの完了待ち、結果のファイルへの反映までを一括で行います。エージェントは「実行して」と 1 回言うだけでよく、ポーリングの制御フローを持つ必要がありません。

パイプでの合成も、制御フローを単純に保つのに効きます。たとえば「ノートブック内の特定の SQL をビルドして、その場で実行する」は、単機能のコマンドをつなぐだけで表現できます。

cdm notebook build-sql ./notebooks/sales.cnb.md -k pageId:p1/sqlId:q1 | cdm sql run -c <connection-id>

エージェントに独自の合成ロジックを書かせるのではなく、Unix 的なパイプで組み合わせられるようにしておくことで、各コマンドは単純なまま、組み合わせで複雑なことができます。

操作の繰り返しを単純にする:ループの形をそろえる

コマンドへの畳み込みと同じ発想で、エージェントに踏ませるループの形も単純にします。とくに、エージェントに「今どれをやるべきか」を判断させる分岐をできるだけ持ち込まないことを意識しました。

分かりやすいのが検証です。ノートブックの中身は、チャート設定・SQL・SQL 同士の参照関係・パラメータ参照……と多岐にわたります。これらを対象ごとに別々の検証コマンドに分けると、一見きめ細かくて良さそうですが、エージェント側には「チャートを直したからチャートの検証を、参照を書き換えたから参照の検証を」というどれを叩くべきかの判断が生まれます。この判断こそが分岐で、抜けや取り違えの温床になります。編集箇所と検証対象の対応をエージェントに正しく判断させるのは、地味に難しい。

そこで cdm では、検証を対象ごとに分けず、validate を一発叩けばノートブック全体が検証されるようにしています。何を編集したかにかかわらず、エージェントがやることは常に同じ validate です。「編集したら validate、エラーが出たらエラーメッセージ(と添えられたドキュメントURL)を見て直して、また validate」という、分岐のない単純なループに収束します。どこを直したかを考えて検証対象を選ぶ、という判断を不要にしました。

流れ全体をエージェントに伝える:ワークフローはドキュメントで渡す

個々の操作とループを単純にしたら、最後に、それらをつないだ作業の流れ全体をエージェントに正しく認識させる必要があります。エージェントが「今どこにいて、次に何をすべきか」を判断できなければ、いくら個々のコマンドが単純でも、全体としては迷走します。

cdm のノートブック操作は、「取得(clone / pull)→ 編集 → 検証・整形(format)→ 実行(run)→ 反映(push)」という一本道に整理してあります。そして、この流れをエージェントに伝える手段は、プロンプトへの作り込みではなくドキュメントです。エージェントには作業開始時にドキュメントを参照させ、そこに書かれた標準的な流れに沿って動いてもらいます。何ができて、どういう順序で進めるべきかは、ドキュメントを読めば分かる、という状態にしています。

このとき、エージェント専用のワークフロー説明をわざわざ用意する必要はありませんでした。作業の流れ自体は、人間が cdm を使うときの流れと同じだからです。人間向けに書いたクイックスタートや各コマンドのドキュメントを、エージェントにもそのまま読ませています。賢いエージェントは、人間と同じドキュメントを読めば、同じワークフローをたどれます。

まとめ

MCP をやめて CLI を主軸にした核心は、ファイルを引数として扱えることでした。エージェントはファイルを直接編集し、変換・検証・実行はツールが引き受ける。その土台の上で意識した設計原則を、3 つに整理しました。

  1. コンテキストを浪費させない — ファイル形式はモデルが効率よく書けるものを選び、ドキュメントは全部積むのではなく必要なときに取りに行かせる。
  2. 間違えさせない努力より、気づいて直せる設計に投資する — プロンプトに禁止事項を積み上げるのをやめ、構造の誤りは検証で・見た目の誤りは画像で気づかせ、機械的に直せるものはツールが直す。
  3. ワークフロー全体をシンプルな一本道に設計する — 多段の手順は 1 コマンドに畳み込み、繰り返しは単純なループにそろえ、その流れは人間と共用のドキュメントでエージェントに伝える。

これらすべての土台にあるのは、冒頭に書いたとおり「エージェントは間違えるし、コンテキストは有限だ」という前提です。そしてもう一つ、今のモデルは十分に賢いという前提もあります。だからこそ、行動を細かく縛るのではなく、賢いエージェントが人間と同じドキュメントを読んで、自律的に判断できるだけの仕組みと情報を、コンテキストを浪費しない形で提供する、というものです。

cdm の詳細仕様については Codatum CLI のドキュメントサイトを、Codatum については codatum.jp をご確認してください。CLI(cdm)はまだベータ版なので、実際に触ってみて気づいたことがあれば、ぜひフィードバックをもらえると嬉しいです(30 日間の無料トライアルあります)。

Discussion

junerjuner

サーバー側で読ませれば参照渡しにもできます。

参照渡しというよりかは「ファイルを参照させることもできます。」でしょうか?

1
g0eg0e

コメントありがとうございます。ご指摘の通りですね。以下の通り修正させて頂きました。

【修正前】厳密には、ローカル MCP サーバーを立ててファイルパスだけを渡し、サーバー側で読ませれば参照渡しにもできます。
【修正後】厳密には、ローカル MCP サーバーを立ててファイルパスを渡せば、サーバー側でファイルを参照させることもできます。

1