Typewriter — 代筆屋をつくる
このプロジェクトでClaude Code(以降Code)と最初に作ったアプリの話をする。
Typewriterという名前の、代筆AIエージェントアプリ。名前の通り、僕の代わりにX(旧Twitter)投稿などの文章を書く役割を担う。
ただ、ここで言う「文章を書く」はXの投稿だけの話じゃない。Slack、メール、Discord。仕事の返信もブログの投稿も、全部ひっくるめて僕の代わりに書くイメージ。相手のメッセージを読んで、文脈を理解して、僕の文体で返す。さらに「この件はタスクとして登録した方がいい」と判断したら、別のタスク管理アプリ(Handler)に情報を渡す。そこまでやって初めて「代筆屋」と呼べる。
なんでそんな大げさなものを最初に作ったのか。理由は単純で、すぐに効果が見えるから。メモリシステムとかタスク管理は重要だけど、作っても地味。Typewriterなら、Xを開けば結果が目に見える。5年間放置していたアカウントに投稿が並ぶ。それだけでモチベーションが全然違うかなと。
で、ここが大事なんだけど、単にX投稿だけだったら実はエージェントなんか要らない。
テンプレート作って、テキスト入力して、APIで投稿。普通のWebアプリで十分。
でも僕が最初から作りたかったのは、複数のデータソースから情報を拾ってきて、文脈を判断して、適切な文章を生成して、場合によっては他のアプリと連携する、そういう複合的なことをやるアプリだった。Slackに来たメッセージの内容を見て「これは急ぎだからHandlerにタスク登録して、返信は丁寧めに」みたいな判断を自律的にやってほしい。
そういう判断を自分でやるためには、中にエージェントがいないと無理だ。だからClaude Agent SDKを使った。
Claude Agent SDK(以降Agent SDK)というのは、Claudeを作ったAnthropicが提供しているエージェント構築用のフレームワーク。ツール実行のループを自動で管理してくれる。トピックを受け取って、下書きを3案作って、フィードバックを受けて修正して、保存する。この「考えて→実行して→結果を見て→また考える」というループを、自分でコード書かなくてもSDKが回してくれる。ファイナンスエージェント、パーソナルアシスタント、カスタマーサポート。公式のユースケースを見ると、まさに僕が作りたいものとドンピシャだった。
システムの全体構成はこうなっている。
中央にClaude(Anthropic API)がオーケストレーター(中心に立って他のエージェントの指揮をする役)として座っている。僕との全会話を担当する「頭脳」。1時間のキャッシュが効くから、長い会話でも文脈を保持できる。
その下にTypewriterなどがぶら下がる。オーケストレーターとTypewriterの間はMCP(Model Context Protocol)という標準プロトコルで繋がっている。オーケストレーターが「Typewriter、下書き作って」と指示を出して、MCPを通じてTypewriterに届く。Typewriterの中にもClaudeがいて(こっちはAgent SDK)、指示を解釈して自分で考えて実行する。
僕 ←→ オーケストレーター(Anthropic API / 頭脳)
↓ MCP
Typewriter(Agent SDK / 代筆屋)
オーケストレーターの中にClaude、Typewriterの中にもClaude。Claudeの中にClaudeがいる、みたいな構成。オーケストレーターは司令塔。Typewriterは実行部隊。それぞれに頭脳があって、それぞれが考える。
少なくとも、そういう設計だった(!)。
MVP(Minimum Viable Product=実用最小限の製品)は意外とすんなりできた。
Mac環境に移行してから着手したんだけど(移行の話は別記。かなり大きなターニングポイントだった)、React(フロントエンド)とPython(バックエンド)で画面を作って、X APIを繋いで、Agent SDKでClaudeを組み込む。Claude Codeに指示を出しながら進めて、それなりに短期間で動くものができた。
MVPが動いた瞬間は素直にうれしかった。TypewriterのUIにはAgent SDKのClaudeとも話せるチャット画面がある。そこでClaudeと会話して、「この話題で投稿を書いて」と頼むと下書きが出てくる。修正して、投稿ボタンを押す。Xに投稿される。5年放置してたアカウントに、新しい投稿が並ぶ(テストだから消したが)。
ここまでくればあとはMCP連携だ。オーケストレーターからTypewriterを直接呼べるようにする。これができれば、僕はオーケストレーターと会話するだけで、Typewriterが裏で動いて投稿まで全部やってくれる。窓口が一つになる!
でもここからが地獄だった。
MCP連携。オーケストレーターとTypewriterを繋ぐ通信の部分。技術的には「MCPサーバーを立てて、MCPクライアントで接続する」だけの話なんだけど、これが動かない。
まず、SDK内蔵のMCPサーバー機能(In-process MCP)がバグっていたのをCodeが見つけた。create_sdk_mcp_server という関数を使うと ProcessTransport is not ready for writing というエラーが出る。Codeが調べたら、SDK v0.1.18の既知バグだった。非同期ジェネレータ(async generator)では修正済みだけど、文字列プロンプト(string prompt)では未修正。GitHub Issueに同じ報告が複数上がっていたらしい。
CodeはIn-process MCPがダメなら外部MCPサーバー(stdio方式)を使おうと決めた。こっちは動くはずだと・・・。
確かに動いた。でも、データベースのパスが壊れていた。
TypewriterのMCPサーバーをオーケストレーター側から起動すると、カレントディレクトリが変わって、相対パスで指定していたSQLiteのデータベースファイルが別の場所に作られてしまう。TypewriterのUIで見える下書きと、MCPサーバー経由で保存される下書きが、別々のデータベースに入っている。つまりオーケストレーターから保存したはずの下書きが、TypewriterのUIに表示されない。
Codeが直した。データベースのパスを絶対パスに変更。次は非同期処理の問題。MCPサーバーの post_to_x 関数がasyncになっていないから、X APIの呼び出しがブロックする。Codeが直した。次は create_post() 関数の引数の型が合っていない。Codeが直した。次は――
こういうのが延々と続いた。一個直すと次のが出てくる。MCPの通信自体は動いているのに、その先の連携がことごとく壊れている。デバッグしてもデバッグしても、新しい問題が湧いてくる。
MCP連携が完全に動いたのはMVPから約2週間後。1日数時間の作業だがかなりかかってしまった。
2週間。コードの量で言えば大した変更じゃない。データベースのパスを絶対パスに変えて、async/awaitを直して、引数の型を揃えて。それだけのことに2週間かかった。
でもこの2週間は重要だった。オーケストレーターからTypewriterを呼べないと、「AIがAIに指示を出す」というエージェントシステムの根幹が成立しない。通信が不安定なままでは何も始まらない。MCPはこのシステムの神経網だ。神経が通ってないのに脳みそだけ立派でも意味がない。
でも2週間の地獄の後、「Typewriterで下書きを作って」と言うと、オーケストレーターがMCP経由でTypewriterに指示を出して、Typewriterが下書きを作って、結果が返ってくるようになった。
ちゃんと動いている。オーケストレーターとTypewriterの間に、やっと神経が通った。
同時期に、もう一つ大きな発見があった。それはAgent Skills(エージェント・スキルズ)。
Anthropicが2025年12月に公開した機能で要は映画Matrixのトリニティのように、ヘリ操縦のスキルをダウンロードしてその時だけ使えるようにするというもの。
これを使って hiro-writing-style というスキルを作った。僕の文体ルール。語尾のパターン、文章の流れ、絶対に使わない表現をスキル定義して、Typewriterに装備させる。
すると、それまで「なんか違う」と思っていた文章が、ちょっとだけ僕っぽくなる。まだ全然足りないけど、方向性は見えた。フィードバックを繰り返せば精度は上がるはずだ。
このSkillsの発見は、後にもっと大きな話に繋がる。メモリから抽出したパターンをスキルファイルに蓄積して、AIの出力を継続的に改善していく。その自動化をLibrarian(メモリ管理エージェント)がやるんだけど、それは別記する。
こうしてTypewriterは「MVPが動く」から「MCP連携が通って、Skillsを装備した、一応ちゃんとしたエージェントアプリ」になった。
構想からここまで、だいたい2〜3週間。長いのか短いのかよくわからない。エンジニアじゃない人間がClaude Codeと一緒に、エージェントシステムの通信基盤を構築して、Skillsの仕組みを発見して実装した。まあ悪くない進捗なんじゃないかな。
MCP連携が安定して、オーケストレーターからTypewriterに指示が飛ぶようになった。技術的には多分「完成」に近い。でも実際に使い込んでみると、致命的な違和感が出てきた。
Typewriterの中にClaudeがいるのに、そのClaudeが何もしていない。
その話は次の記事で。
No Comments