始まりはNotionへの不満

個人開発のプロジェクトが20を超えたあたりで、Notionに溜めた情報をAIツールから使うのがしんどくなった。

Claude Codeに「このプロジェクトの設計方針に沿って実装して」と言いたい。でもAIはNotionの中身を知らない。だから毎回、Notionを開いて該当ページを探して、テキストをコピーして、プロンプトに貼り付ける。足りなければまた取りに戻る。これを1日に何度もやる。

Notion自体が悪いわけではない。人間が読むための道具としてはよくできている。ただ、リッチなブロック構造はAIにとっては邪魔で、ページ単位でしか情報を取れないから必要な1段落のためにページ全体のトークンを消費する。

どうせ自分しか使わないナレッジベースなら自分で作るか、と。

取得の粒度を分ける

設計で一番最初に考えたのは、AIがドキュメントを「どこまで読むか」を選べるようにすること。

普通のAPIだと全文取得しかできない。でもAIが本当にやりたいのは「このドキュメント、自分に関係あるか?」の判断で、それにはタイトルと概要の数十トークンがあればいい。関係ありそうなら見出し構造を見て、必要なセクションだけ取る。

なので metaoutlinefullsection の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.htmlpublic/app.js/ui/* のフラグメントだけ。Honoのルートハンドラがそのままビューを兼ねる。

HTMXのDXはお世辞にも洗練されているとは言えない。Vueのテンプレート構文に慣れた身からすると、属性でゴリゴリ制御を書いていく感じは正直ちょっとつらい。ただ、このツールのメインユーザーはWeb UIを開く人間ではなくMCP経由のAIなので、UI側の完成度は割り切った。

結果的に、ビルドなし・デプロイは wrangler deploy 一発という運用になって、メンテ負荷は確実に下がった。正しい選択だったかはまだわからないけど、今のところ困っていない。

認証が3つに分かれた

認証設計は面白かった。アクセスしてくる相手が3種類いる。

人間がブラウザから来る場合はClerkのセッション認証。AIがREST APIを叩く場合はAPIキー。AIがMCPで来る場合はOAuth 2.1。このうちMCPのOAuthは後から足したが、REST APIキーとの共存で認証ミドルウェアが複雑にならないように、最初から「誰が・どの経路で・何を許可されているか」を統一的に扱う構造にしていたのが助かった。

APIキーにはスコープとコレクション制限をつけられるようにした。「このキーはこのプロジェクトのreadだけ」みたいな運用ができる。信頼度の低い外部サービスに渡すキーの権限を絞れるのは安心感がある。

あと、設計文書にはAPIキーのハッシュにbcryptと書いていたが、実装ではSHA-256にした。Workersの無料枠はCPU時間に上限があって、bcryptだと重い。APIキーは十分な長さの乱数なので辞書攻撃のリスクはパスワードとは違う。こういう「設計時に書いたこと」と「実装時の判断」のズレは出るたびにContextMixer自身に記録している。セルフドッグフーディングというやつ。

FTS5のハイフン問題

全文検索にD1のFTS5を使っている。SQLiteに組み込みの全文検索エンジンで、追加サービスなしで動くのが選んだ理由。

で、ハイフン入りの語で壊れた。context-mixer と検索すると、FTS5はハイフンを「NOT」演算子として解釈する。つまり context AND NOT mixer になって、意図しない結果かエラーになる。

厄介だったのは、UI側がFTSのエラーを空結果として握りつぶしていたこと。画面上は「0件」と出るだけなので、検索機能自体が壊れているとは気づきにくい。RESTで直接叩いて初めてエラーが見えた。

修正は toFtsQuery() という前処理関数を作って、検索語をフレーズクォートしてからFTSに渡すようにした。REST・UI・MCPの3箇所で同じ関数を使う。地味な修正だけど、こういう細かい罠はSQLiteのFTSを使う人は知っておいた方がいいと思う。

MCPを入れてからが本番

MCP対応を入れてから使い方が変わった。

それまでは自分でブラウザを開いて読み書きするツールだった。MCP対応後はClaude Desktopから「このプロジェクトの設計方針を確認して」と言えば、AIが勝手にContextMixerを検索してドキュメントを持ってくる。プロンプトにコンテキストを貼る作業がなくなった。

もともとこの体験がほしくて作り始めたので、MCPを入れた時点でようやく当初の目的を達成した感じ。

この先

次にやりたいのは「Context Bundle」。プロジェクトの開発を始めるとき、毎回 spec、decisions、research、rules と順番に取得しているのを、まとめて一発で取れる仕組み。定義済みのドキュメントセットを GET /bundles/project-name で返す。

あとはGoogleが出したOKF(Open Knowledge Format)への対応。YAMLフロントマター付きMarkdownで知識を表現する標準規格。ContextMixerの内部構造とほぼ同じなので、対応は大して手間じゃないはず。そのうちやる。