Common Lisp環境とAIエージェントを繋ぐ cl-mcp の活用事例:AIのための構造化grepをAIに作らせる
なぜ今AI開発にCommon Lispを使うのか
ここ最近、AIエージェントによるコード生成が当たり前のものとなってきましたが、そこで選択されるプログラミング言語はTypescriptやPythonが主流になっています。これらの言語がよく使われる理由としては、圧倒的に多くのデータセットを持っていること、型注釈が付けられることなどが挙げられます。
一方で、Lispでは環境との相互作用によって開発を進めていくというスタイルが取られています。
AIエージェントによる開発では、AIエージェントが自律的に試行錯誤を繰り返すことによって開発を進めていくため、実はLispの対話的開発プロセスと相性が良いのではないかと考えました。
Lispでは式評価ベースで非常に小さい単位で試行錯誤のループを回すことができるので、AIに評価、改善のループを高速回転させるのに向いています。AIの本質的な優位性は疲れを知らないことだからです。
このようなCommon LispのAIエージェントによる開発を支援するため、最近、clojure-mcp を参考にして cl-mcp というプロジェクトの開発を行っています。これがそこそこ使えるようになってきたため、この記事ではcl-mcpの導入と実際にClaude Codeに開発させたプロジェクトの例を示します。
題材として選んだのは、Common Lispコードの構造を理解する検索ツール clgrep です。
既存の行単位の grep よりも、S式(フォーム)単位でコードを取得できた方がAIは文脈を正確に理解できます。これは将来的にcl-mcp自体に取り込むことを見据えたプロジェクトです。
プロジェクトの概要: clgrep
作成するのは clgrep というライブラリです。以下のようなコマンドラインインターフェースも付けます。
- 人間向け機能: 普通の grep のように使える(再帰検索、行番号表示など)。
- AI向け機能:
--jsonオプションで、マッチした箇所の「トップレベルフォーム全体」をJSONで返す。
前提としてRoswellとClaude Codeはインストールされているものとします。インストールされていない場合は以下のようにインストールしてください。
Roswellのインストール (Homebrewの例)
brew install roswell
Claude Codeのインストール
npm install -g @anthropic-ai/claude-code
1. 環境構築: LispとAIを接続するMCPサーバcl-mcp
まずは、Claude Code(MCPクライアント)からCommon Lisp環境(MCPサーバー)を操作できるようにします。ここでcl-mcpを導入します。
インストールとプロジェクト作成
Roswellがインストールされていれば以下のようにして比較的簡単に導入できます。
# 1. 必要なツールのインストール
ros install cl-ai-project/cl-mcp
ros install fukamachi/cl-project
# 2. プロジェクトの雛形作成
make-project clgrep
cd clgrep
# 3. cl-mcp推奨プロンプトをコピーする(AIにREPL駆動開発を教えるためのプロンプト)
cp -r ~/.roswell/local-projects/cl-ai-project/cl-mcp/prompts ./
Claude CodeへのMCPサーバ登録
このプロジェクト内で、Claude Codeが cl-mcp を認識できるように設定します。
これには2通りの方法があります。
まずスタンドアロンでMCPサーバを起動する方法です。こちらはClaude Codeの起動時にMCPサーバもセットで起動する方法で、簡便ですが作業中のLisp処理系には介入できません。以下のコマンドによって設定でき、プロジェクトルートに .mcp.jsonというファイルができます。
claude mcp add cl-mcp -s project -- ros run -s cl-mcp -e "(cl-mcp:run :transport :stdio)"
次に、既に起動中のREPLに接続する方法について紹介します。こちらの方がLisp処理系への介入の自由度が高くおすすめですが、手順が煩雑になります。
- step1: 起動中のREPLからcl-mcpをロードし、ローカルホストの適当なポート番号でTCP接続を待ち受けます(ここでは12345とします)
(ql:quickload :cl-mcp)
(cl-mcp:start-tcp-server-thread :host "127.0.0.1" :port 12345)
- step2: cl-mcpにはstep1で開いたポートにTCP接続し、stdioに変換するPythonスクリプトが付属しています。以下のようにClaude Codeに設定できます。
claude mcp add cl-mcp -s project -- python3 ~/.roswell/local-projects/cl-ai-project/cl-mcp/scripts/stdio_tcp_bridge.py --host 127.0.0.1 --port 12345
これで準備完了です。Claudeを起動して接続を確認します。
claude
起動後にプロンプトに /mcp と入力し、以下のように表示されていれば接続されています。
❯ 1. cl-mcp ✔ connected · Enter to view details
これで、ClaudeはREPLを通じてLispコードを実行したり、プロジェクト内のファイルを自由に読み書きできるようになりました。
2. 実装プロセス: AIによる段階的開発
ここからは、実際にClaudeに指示を出してコードを書かせていきます。複雑な要件を一気に投げると失敗するため、「ロジックの核」→「周辺機能」→「CLI化」とステップを分けて指示を出しました。
Step 1: まずは基本のgrep機能
まずは手慣らしとして、単純な正規表現マッチングを作らせます。
Prompt 1:
Common Lispを使って、簡易的なgrepコマンドを作成してください。
【要件】
* コマンドライン引数で「検索パターン(正規表現)」と「対象ファイルパス」を受け取る。
* ファイルを読み込み、パターンに一致する行を標準出力に表示する。
* 一致した行には行番号を表示する(grep -n相当)。
* cl-ppcreライブラリを使用して実装すること。
Step 2: Lisp構造認識(S-Expression Awareness)
ここが今回やりたかったことの中核です。行単位ではなく「トップレベルフォーム」単位で抽出させます。また、安全のために read 関数を使わず、テキスト解析で括弧の対応を見るように指示します。
Prompt 2.0:
Common Lispで、指定された行番号が含まれる「トップレベルフォーム(Top-level Form)」をテキスト全体から切り出す関数 extract-toplevel-form を作成してください。
【要件】
入力: content (ファイル全体の内容が入った文字列), target-line-number (整数)
処理:
* read関数は使わず、文字ごとのスキャンと括弧のカウントで構造を把握してください(堅牢性のため)。
* target-line-number の行が含まれている一番外側のS式(( で始まり対応する ) で終わる範囲)を特定します。
* コメントや文字列リテラル内の括弧に惑わされないように注意してください。
出力:
抽出されたフォームの文字列。
もしフォームが2000文字を超える場合は、対象行の前後5行程度を残し、それ以外を ... で省略した文字列を返してください。
Step 3: ファイルシステム探索と .gitignore
実用的なツールにするため、再帰検索と、プロジェクトでGit管理から除外されているファイルを検索対象外にする部分を実装します。
Prompt 2.1:
次に、ディレクトリを再帰的に探索し、.gitignore を考慮してファイルリストを返す関数 collect-target-files を作成してください。
【要件】
入力: root-directory
処理:
* uiop を使用してディレクトリを走査する。
* gitignore があれば読み込み、そのGlobパターンを cl-ppcre 正規表現に変換してフィルタリングする。
* git ディレクトリ自体は無視する。
* 拡張子が .lisp, .asd, .ros のファイルのみ収集する。
出力: ファイルパス(pathname)のリスト。
Glob変換用のヘルパー関数も実装してください。
Step 4: ロジックの統合
これまでの部品を semantic-grep 関数として統合します。
Prompt 2.2:
最後に、これらを組み合わせて MCPサーバー用のメイン関数 semantic-grep を実装してください。
【仕様】
* collect-target-files でファイル一覧を取得。
* 各ファイルを読み込み、cl-ppcre でキーワード検索を行う。
* ヒットしたら extract-toplevel-form を使って文脈を取得。
* 結果を以下の連想リスト(Alist)のリストとして返す。 (((:file . "...") (:package . "...") (:form . "...") ...) ...)
* ファイル内の (in-package :xxx) を正規表現で簡易解析し、:package フィールドに埋める処理も追加してください。
Step 5: CLIツールとしてのパッケージング
最後に、これを clgrep コマンドとして使えるようにします。ここで重要なのが --json オプションの追加です。
Prompt 3:
次に、作成した semantic-grep のロジックを使って、単体で動作するコマンドラインツール clgrep を作成します。
ros init clgrep で生成される clgrep.ros をベースに実装してください。
**【実装要件】**
1. 依存関係のロード: (ql:quickload '(:cl-ppcre :uiop :yason)) を明記。
2. ロジックの統合: 作成した関数群を定義。
3. CLIインターフェース:
* -r (再帰), -i (大文字小文字無視) の実装。
* --json: 結果をJSON形式で標準出力する(AIエージェント連携用)。
4. 出力フォーマット:
* --json 指定時はJSON配列。
* 通常時は人間が見やすい ファイル:行: コンテンツ 形式。
この仕様で clgrep.ros の完全なコードを書いてください。
3. 完成、動作デモ
このようにして、ほぼClaudeとの対話だけでCommon Lisp製のgrepツールが完成しました。この間ほとんどコードには手を入れていません。
完成品がこちらです。
clgrep: https://github.com/masatoi/clgrep
インストール
このツールは以下のようにGithubからインストールできます。
ros install masatoi/clgrep
~/.roswell/binにパスが通っていればそのまま使えるはずです。
人間が使う場合
普通にソースコードを検索できます。-r で再帰的な検索もできます。
$ clgrep -r defun .
/path/to/src/main.lisp:12: (defun grep-file (pattern filepath)
/path/to/src/main.lisp:25: (defun main ()
...
AIエージェントが使う場合 (--json)
ここが本ツールの真骨頂です。--json をつけると、AIが解析しやすい構造化データが出力されます。
$ clgrep --json defun src/main.lisp | jq .
[
{
"file": "/Users/user1/cl/clgrep/src/main.lisp",
"line": 12,
"match": "(defun grep-file (pattern filepath)",
"package": "CLGREP",
"form": "(defun grep-file (pattern filepath)\n \"Search for PATTERN in FILEPATH...\"\n (let ((match-count 0))...))"
},
{
"file": "/Users/user1/cl/clgrep/src/main.lisp",
"line": 25,
"match": "(defun main ()",
"package": "CLGREP",
"form": "(defun main ()\n \"Entry point for the clgrep...\"\n (let ((args (uiop:command-line-arguments)))...))"
}
]
ここで注目すべきは "form" フィールドです。マッチした行だけでなく、その関数定義全体(defun ...)が含まれています。 また、(in-package) を解析してパッケージ名も付与されています。
これをAIエージェントに渡すことで、AIはこの関数がどのような引数を取り、どのような処理をしているかを一目で理解でき、追加でファイルを読みに行く回数を減らすことが期待できます。
まとめ
この記事では、cl-mcp と Claude Code を組み合わせることで、ほぼ自律的にコードを書かせる例を示しました。
単純にAIエージェントにCommon Lispを書かせると括弧の対応を微妙に間違えていたり、通常人間がやる開発プロセスで活用するデバッガや処理系からの情報を使えていなかったりしたため、MCPサーバを介することで構造化編集や処理系からの情報収集を肩代りさせられるようにしました。
こういった枝葉のところで詰まることがなくなったため、開発がスムーズに進むようになったと感じています。
Discussion