始まりはNotionへの不満
個人開発のプロジェクトのドキュメントをNotionで管理していました。しかし使っていくとアクセスが遅いのが気になり始めました。少しの変更でもそこそこ時間がかかり、何度も読み書きしようとするとアクセスのレイテンシとトークン消費が結構大きいなと思い始めて、ほかのサービスやアプリも検討してみましたが、チャットアプリからもCLIのエージェントツールからもアクセスしやすくAI向きなものがなかなか見つかりませんでした。
Notion自体が悪いわけではないです。人間が読むための道具としてはよくできている。ただ、リッチなブロック構造はAIにとっては邪魔で、ページ単位でしか情報を取れないから必要な1段落のためにページ全体のトークンを消費してしまいます。 どうせ自分しか使わないナレッジベースなら自分で作るか、と思ったのが始まりです。
取得の粒度を分ける
設計で一番最初に考えたのは、AIがドキュメントを読む際に最小限の内容を取得できるインターフェイスで、AIが「どこまで読むか」を選べるようなAPI設計にすることでした。 普通のAPIだと全文取得しかできない。でもAIが本当にやりたいのは「このドキュメント、自分に関係あるか?」の判断で、それにはタイトルと概要の数十トークンがあればいい。関係ありそうなら見出し構造を見て、必要なセクションだけ取る、という使い方ができるようにすればトークン効率が高くなるのではないかということです。
なので meta、outline、full、section の4段階の設計にして、図書館で目次だけ見て必要な章を開く、そんなことをAPIでやれるようにした感じです。地味だけど、これでトークン消費がだいぶ減ったと思っています。
Cloudflareで全部まかなう
スタックはCloudflare Workers + D1 + R2です。全部Cloudflareに寄せたのはコストの問題です。
ナレッジベースって一度作ったら何年も使うものだし、個人で使うだけなのでできるだけ安く運用できるようにしたかった。使わない月にも金がかかるのは精神的に良くないし。Cloudflareの無料枠は個人利用ならかなりの量使えます。Workers 10万req/日、D1 5GB、R2 10GB。 フレームワークはHonoにしました。Workers上で動くものの中で一番取り回しがいい。D1がSQLiteベースなのでFTS5(全文検索)がそのまま使えて、検索エンジンを別に立てなくていいのが地味にでかいです。
VueをやめてHTMXにした
フロントエンドは最初Vue MPAで設計しました。仕事でVue.jsを使っているし、コンポーネントの切り方とかも慣れている。同一Workers上でMPAとして返せばSPAほど複雑にならないだろうと。 で、少し書き始めたところで、ContextMixerのUIでやることって、一覧を出す、中身を表示する、テキストエリアで編集する、くらいしかない。状態管理らしい状態管理もないし、Vueを入れるとビルドステップが増えるし、Workersへのデプロイも一手間余計にかかる。
前から少し気になっていた、HTMXを試してみることにしました。
サーバーからHTMLフラグメントを返すだけでUIが更新される仕組みで、ビルドステップがなくなる。public/index.html と public/app.js と /ui/* のフラグメントだけ。Honoのルートハンドラがそのままビューを兼ねます。
実際のコードはAIが書いてる部分がほとんどですが、レビューで見るのでなんとなくHTMXがわかってきたかなといった感じです。
Vueのテンプレート構文に慣れた身からすると、属性でゴリゴリ制御を書いていく感じは正直ちょっと抵抗があります。ただ、手軽なのは確かでビルドなし・デプロイは wrangler deploy 一発という運用になったりと利点も結構大きいと感じてます。
いったんHTMXで進めて今後人間向けのUIを拡張していく中でどの程度HTMXで行けるのか、どこは大変なのかを見ていければと思っています。本当に厳しい部分があればVueで作り直すことも考えたいと思ってます。
認証が3つに分かれた
認証設計は慣れておらず少し苦労しました。アクセスしてくる相手が3種類いる。 人間がブラウザから来る場合はClerkのセッション認証。AIがREST APIを叩く場合はAPIキー。AIがMCPで来る場合はOAuth 2.1。このうちMCPのOAuthは後から足したのですが、ClaudeがREST APIキーとの共存で認証ミドルウェアが複雑にならないように、最初から「誰が・どの経路で・何を許可されているか」を統一的に扱う構造を提案してくれていたのが助かりました。
APIキーにはスコープとコレクション制限をつけられるようにしました。「このキーはこのプロジェクトのreadだけ」みたいな運用ができる。信頼度の低い外部サービスに渡すキーの権限を絞れるのは安心感があります。
FTS5のハイフン問題
全文検索にD1のFTS5を使っています。今回触るまで知らなかったのですが、SQLiteに組み込みの全文検索エンジンで、追加サービスなしで動くのが選んだ理由です。Claudeがいろいろ教えてくれました。
最初はスムーズにできたかなと思ったのですが、ハイフン入りの語で壊れました。context-mixer と検索すると、FTS5はハイフンを「NOT」演算子として解釈する。つまり context AND NOT mixer になって、意図しない結果かエラーになる。
厄介だったのは、UI側がFTSのエラーを空結果として握りつぶしてしまっていたことです。画面上は「0件」と出るだけなので、検索機能自体が壊れているとは気づきにくい。RESTで直接叩いて初めてエラーが見えました。
修正は toFtsQuery() という前処理関数を作って、検索語をフレーズクォートしてからFTSに渡すようにしました。REST・UI・MCPの3箇所で同じ関数を使っています。
MCPを入れてからが本番
MCP対応を入れてから本格的にNotionのドキュメントを移して運用を始めました。 それまでは自分でブラウザを開いて読み書きするだけでしたが、MCP対応後はClaude Desktopから「このプロジェクトの設計方針を確認して」と言えば、AIが勝手にContextMixerを検索してドキュメントを持ってくる。プロンプトにコンテキストを貼る作業がなくなりました。
もともとこの体験がほしくて作り始めたので、MCPを入れた時点でようやく当初の目的を達成した感じです。
この先
次にやりたいのは「Context Bundle」です。プロジェクトの開発を始めるとき、毎回 spec、decisions、research、rules と順番に取得しているのを、まとめて一発で取れる仕組み。定義済みのドキュメントセットを GET /bundles/project-name で返す想定です。
あとは人間向けのUIのリッチ化と、Googleが出したOKF(Open Knowledge Format)への対応。YAMLフロントマター付きMarkdownで知識を表現する標準規格。ContextMixerの内部構造とほぼ同じなので、対応は大して手間じゃないはず。そのうちやりたいです。