エージェント型のAIは、ターミナルでコマンドを実行できます。つまり、自分たちで作ったツール(CLI)をAIに使わせることができます。
ただ、人が使う前提で作ったツールを、そのままAIに使わせるとうまくいかないことがあります。出力の形がバラバラで読み違える、失敗したのか成功したのか分からない、どのツールを使えばいいか迷う、などです。
この記事では、AIエージェントから呼ばれることを前提にしたCLIの作り方を紹介します。題材は、業務の効率化ツールをまとめた devtools-workspace と、その共通ライブラリ devtools-common です。どれも、特定の会社やシステムの名前を含まない汎用版として公開しています。
なぜMCPではなくCLIなのか
AIに道具を渡す方法としては、MCPサーバーもよく知られています。このツール群では、あえて「GitHubからclone → セットアップ → Copilotからコマンドを呼ぶ」という形にしました。理由は、ドキュメントにこう書いています。
| 選択 | 理由 |
|---|---|
| clone + CLI + プロンプトファイル(採用) | 追加の設定・常駐プロセスが不要。Copilotは普段どおりターミナルでコマンドを実行するだけ。ツールはCopilotが無くても単体で使える |
| MCPサーバー(今は見送り) | Copilotがツールを自動で見つけられる利点はあるが、MCPの有効化・設定ファイル・組織のポリシー確認が必要で、初心者の最初の一歩が重くなる |
ポイントは「ツールはAIが無くても単体で使える」ことです。AIに頼れないときも人が直接使えますし、テストも普通のCLIとして書けます。
また、各CLIの出力形式と終了コードをそろえておけば、必要になったときにMCPの薄いラッパーを後から足すこともできます。CLIを先に作っておくのは、後戻りしにくい選択ではありません。
約束1:機械が読める出力を用意する
AIにツールの結果を読ませるとき、人向けの整形された出力は向いていません。見た目が変わるたびに、読み違いが起きるからです。
そこで、全ツールで共通の「レポート形式」を決めています。-f json を付けると、次の形で結果を返します。
{
"schema_version": "1.0",
"tool": "fieldcheck",
"tool_version": "0.2.0",
"title": "fieldcheck 検証レポート",
"meta": { "input": "expense.csv", "records": 6 },
"summary": { "error": 1, "warning": 0, "info": 0 },
"findings": [
{
"rule_id": "EXP-003",
"severity": "error",
"message": "合計 が 金額 + 税額 と一致しません",
"location": "row 2",
"field": "合計",
"expected": "8800",
"actual": "8900",
"suggestion": "合計 または内訳(金額, 税額)を確認してください"
}
]
}
工夫しているところは3つです。
-
summaryで全体が一目で分かる。 AIは件数を見て、詳しく読むべきかを判断できます。 -
expectedとactualとsuggestionがある。 「何が違って、どう直すか」まで書いてあるので、AIが人に説明しやすくなります。 -
schema_versionがある。 形式を互換性のない形で変えたときは、メジャー番号を上げます。使う側は、メジャーが想定どおりかを確かめます。
人が読むためのMarkdownやHTML形式も用意していますが、そちらは「見た目は予告なく変わることがある」と明記しています。AIや他のツールから使うときは、JSONを使ってもらう約束です。
約束2:標準出力にはレポートだけを出す
ログや進捗、警告は、すべて標準エラーに出します。標準出力には、レポートだけを出します。
doc2md 報告書.pdf -d out/ -f json > report.json
こうしておけば、出力をそのままファイルに保存しても、ログが混ざって壊れることがありません。AIがコマンドの結果を読むときも、どこからがレポートかで迷いません。
約束3:終了コードの意味をそろえる
AIは、コマンドの終了コードを見て成功・失敗を判断します。そこで、全ツールで意味をそろえています。
| コード | 意味 | 使う側の扱い |
|---|---|---|
0 |
正常終了。指摘なし | 成功 |
1 |
正常に処理したが、指摘がある | レポートの findings を確認する |
2 |
使い方・設定ファイル・入力の誤り(前提が崩れている) | データではなく、引数・設定・列名を直す |
3 |
想定外の実行時エラー | 不具合として報告する |
特に大事なのが、1と2を分けていることです。
- 1は「ツールはちゃんと動いて、データに問題が見つかった」
- 2は「そもそもツールの使い方が間違っている」
これが同じ番号だと、AIは引数の間違いを「データの問題」と思い込み、データを直そうとしてしまいます。番号を分けておけば、AIは何を直すべきかを取り違えません。
実装では、共通ライブラリに終了コードを定義して、全ツールで使い回しています。
class ExitCode(IntEnum):
OK = 0 # 正常終了・指摘なし
FINDINGS = 1 # 正常に処理したが error 重大度の指摘がある
USAGE = 2 # 引数・設定ファイル・入力スキーマの誤り(前提が崩れている)
RUNTIME = 3 # 想定外の実行時エラー
約束4:どのツールを使うかをAGENTS.mdに書く
ツールが増えると、AIはどれを使えばいいか迷います。そこで、ワークスペースの AGENTS.md に「いつ・どのツールを使うか」の一覧を書いています。抜粋するとこんな形です。
| やりたいこと | コマンド |
|---|---|
| Word / Excel / PowerPoint / PDF / draw.io の中身を読みたい |
doc2md <file> -d out/ -f json → out/<stem>.md を読む |
| 表の項目間の整合性を確かめたい | fieldcheck validate <data> -r rules.yaml -f json |
| 設計書の新旧の差分を見たい | design-diff excel --type api --before 旧.xlsx --after 新.xlsx |
| Officeの文字列を一括で差し替えたい |
必ず先に office-replace <dir> --rules rules.csv --dry-run、確認後に本番 |
ポイントは、ツール名ではなく「やりたいこと」で引けるようにしていることです。AIは「Excelの中身を読みたい」という状況から、使うべきツールにたどり着けます。
注意書きも一緒に書いています。
- 既存のツールで足りることを、新しいスクリプトにしない
-
doc2mdのJSONにloss.*の警告があれば、変換で内容が欠けた可能性がある。利用者に伝える - 終了コード2は、データではなく前提の誤り。データではなく前提を確認する
最後の項目は、約束3の終了コードを、AIへの指示としても念押ししているものです。
約束5:危ない操作は、先に確認できるようにする
ファイルを書き換えるツールには、--dry-run を用意しています。
office-replace samples/input --rules samples/rules.xlsx -r --dry-run
--dry-run では、どこが置換されるかのレポートだけを出し、ファイルは書き換えません。さらに、本番でも元のファイルは書き換えず、別のフォルダに出力します。
AGENTS.mdにも「必ず先に --dry-run」と書いているので、AIはいきなり書き換えません。取り返しのつかない操作ほど、確認の手順をツールとAIへの指示の両方に入れておきます。
約束6:機密を外に出さない
業務の文書を扱うツールなので、データの扱いにも気を配っています。
- ツールはPCの中だけで動き、ネットワークに接続しない。生成AIのAPIも呼ばない
- 一時ファイルは作成者だけが読めるフォルダに作り、処理後に必ず削除する
- ログには本文を出さず、件数やファイル名だけを出す
ログに本文を出さないために、共通ライブラリには本文の代わりに長さだけを返す関数を用意しています。
def describe_text(text: str) -> str:
"""本文の代わりにログへ出す要約(長さのみ)."""
return f"<text {len(text)} chars>"
AIを使うときの注意も書いています。Copilotには、チャットの内容やCopilotが読んだファイル・コマンド結果が送られます。そこで、元の文書を直接開かせずに変換結果を読ませる、検証はツールに任せて指摘のあった行だけを説明させる、といった使い方をすすめています。
自分のツールに取り入れる手順
-
出力に
-f jsonを足す。 最低限、summaryとfindingsの一覧を返す - ログを標準エラーに移す。 標準出力にはレポートだけを出す
- 終了コードを決める。 少なくとも「指摘あり」と「使い方の誤り」を別の番号にする
- 書き換えるツールに
--dry-runを付ける -
リポジトリに
AGENTS.mdを置く。 「やりたいこと → コマンド」の表と、注意書きを書く - AIに一度使わせてみる。 迷ったところや読み違えたところを、AGENTS.mdかツールの出力に反映する
最後の手順が大事です。AIがつまずいたところは、人間の新人もつまずくところです。
よくある失敗
人向けの出力しか用意しない
罫線や色付きの表は、人には読みやすくてもAIには読みにくいです。機械が読む形式を別に用意しましょう。
ログとレポートを同じ出力に混ぜる
標準出力にログが混ざると、結果をファイルに保存したときに壊れます。AIも、どこからが結果か分からなくなります。
失敗をすべて同じ終了コードにする
「データに問題がある」と「使い方が違う」が区別できないと、AIは見当違いのところを直そうとします。
ツールの使い分けを書かない
ツールが増えるほど、AIは使うべきものを選べなくなります。AGENTS.mdに「やりたいこと」から引ける一覧を置きましょう。
まとめ
AIエージェントにとって、CLIは手と道具です。道具が扱いやすければ、AIの仕事の質も上がります。
- 機械が読める出力(JSON)と、標準出力・標準エラーの使い分け
- 意味のそろった終了コード
- 「やりたいこと」から引けるAGENTS.md
- 危ない操作の前の
--dry-run
ハーネスの記事で、Toolsは「PCと社内システムのアカウント」にあたると書きました。自分たちで作るCLIは、そのToolsを増やすことです。AIに使わせる前提で作っておけば、人にとっても使いやすいツールになります。
この記事は Zenn にも投稿しています。