はじめに
MCPサーバーを自作したのは今回が初めてだ。
正直、「既存のサーバーをつなぐだけで十分では?」と思っていた。自分のMarkdownメモをAIに渡せればいいだけなら、ファイルを直接渡せばいい話だと考えていた。
ただ、4,000件を超えるメモが溜まったタイミングで限界を感じた。ファイルをそのまま渡すのは無理だし、grepはもう機能していない。「あのメモどこだっけ」で毎回5〜10分消える状況が続いていた。
MCPとQdrantを組み合わせて、自分のメモ全体をAIの記憶にしてみた。実際に動かしてみると、考えが変わった。
ローカル検索の限界
ローカルのMarkdownファイル群を検索するとき、自分はずっとgrepかObsidianの全文検索を使っていた。
これで困るのは、「キーワードを正確に覚えていない」ケースだ。
たとえば「DynamoDBで楽観的ロックをどう実装したか」を調べたいとする。自分が書いたメモには「ConditionExpression」とか「TransactWrite」という言葉しか出てこない。「楽観的ロック」というキーワードで検索しても、そのメモはヒットしない。
ファイル名検索はさらに厳しい。過去の自分が付けたファイル名なんて覚えていない。dynamodb-optimistic-lock.md という名前で保存していれば見つかるが、dynamodb-transaction-impl.md と付けていたら終わりだ。
| 問題 | 原因 |
|---|---|
| キーワードが一致しないとヒットしない | grepは字句一致のみ |
| ファイル名が思い出せない | 命名規則が自分の記憶に依存 |
| 関連知識が芋づる式に出てこない | ファイル間の意味的なつながりを検索できない |
| 「あのメモどこだっけ」で毎回時間を消耗 | 検索コストが高い |
正直ぞっとした。4,000件メモを溜めてきたのに、半分以上はほぼ検索不能な状態だったかもしれない。
作ったもの
構成はシンプルだ。
Markdownファイル群(4,204件)
↓
recall(投入専用CLI・自作)
↓
Qdrant(ローカルDocker)
↓
mcp-server-qdrant(既存MCP)
↓
Claude Code
ポイントは「検索側は自作していない」ことだ。mcp-server-qdrant という既存のMCPサーバーがすでに存在し、QdrantにつないでClaude Codeから検索できる状態にしてくれる。自作したのは「MarkdownファイルをQdrantに投入するCLI(recall)」だけだ。
Obsidianは単なるMarkdownファイルの保管場所として使っている。Obsidian固有の機能には依存していない。
Dockerの起動とMCP登録は以下で完了する。
# Qdrant起動
docker run -p 6333:6333 qdrant/qdrant
# MCP登録
claude mcp add recall --scope user -- \
uvx mcp-server-qdrant \
--qdrant-url http://localhost:6333 \
--collection-name knowledge-atomic \
--embedding-model intfloat/multilingual-e5-large
初期同期(4,204ファイル)はIntel MacのCPUで約2.5時間かかった。終わると4,484ポイントがQdrantに格納された。
ローカル検索 vs MCP:実際の比較
「DynamoDB楽観的ロック」を題材に比較してみる。
recall なし(Claude内部知識のみ)
Claudeへの質問:「DynamoDBで楽観的ロックを実装するときのポイントは?」
返ってきた回答
DynamoDB の楽観的ロックは、条件付き書き込み(ConditionExpression)を使って実装します。バージョン番号属性をアイテムに持たせ、更新時に「現在のバージョンが期待値と一致する場合のみ書き込む」条件を指定します...
正しい。ただし教科書的だ。「知識として知っている」レベルの回答であり、自分のコードベースとは無関係だ。
recall あり(MCP経由)
同じ質問をすると、自分が実際にメモしていた情報からTypeScriptコードが返ってきた。
-
ConditionExpressionの実際の実装コード - Redisを使わなかった理由(設計判断)
-
TransactWriteで4プレイヤーをアトミックにクレームする実装 - リトライは最大5回 + exponential backoff
- 芋づる式に関連ノートも取得(シングルテーブル設計、インデックス設計、ORM落とし穴)
| 観点 | recall なし | recall あり |
|---|---|---|
| コード | なし(一般的なサンプルのみ) | メモに記載されているTypeScriptコード |
| 設計の背景 | なし | なぜRedisを使わなかったか、どのプロジェクトで使ったか |
| 関連知識 | なし | シングルテーブル設計・インデックス設計・ORM落とし穴まで横断 |
| 出典 | なし | どのプロジェクトから得た知識かトレーサブル |
| 粒度 | 汎用的 | 自分固有の実装・判断 |
ここで気づいたことがある。「自分固有の実装・設計判断はClaudeの内部知識には存在しない」という当たり前の事実だ。
Claudeはどれだけ賢くても、自分が過去にメモしていたコードは知らない。なぜその設計を選んだかも知らない。それを補えるのは自分のメモだけだ。MCPはその橋渡しをしている。
実装してわかったこと
mcp-server-qdrantの存在を最初知らなかった
最初は「Qdrant検索のMCPも自作しないといけない」と思っていた。調べてみると mcp-server-qdrant がすでに存在し、ほぼそのまま使えた。これは思わぬ発見だった。
自作の範囲を「投入CLI(recall)」だけに絞れたことで、実装コストが下がった。
Embeddingモデルの選定が最初のハマりポイント
当初はBGE-M3を使おうとした。多言語対応で精度が高いという評判だったからだ。(BAAI/BGE-M3 モデルページ)ところがfastembedに未対応だった(Issue #348がオープンのまま)。
結果として intfloat/multilingual-e5-large(1024次元)に変更した。日本語メモが多い自分の用途には十分な精度が出ている。
| モデル | fastembedサポート | 多言語対応 | 採用 |
|---|---|---|---|
| BGE-M3 | ✗(Issue #348) | ◎ | 不採用 |
| multilingual-e5-large | ✓ | ○ | 採用 |
| text-embedding-ada-002 | ✗(OpenAI API必要) | ○ | 不採用 |
E5モデルのprefix規約
multilingual-e5-large にはprefix規約がある。(intfloat/multilingual-e5-large モデルページ)
- ドキュメント(投入時):
passage:を先頭に付ける - クエリ(検索時):
query:を先頭に付ける
無視しても動く。ただし精度が下がる。ドキュメントに書いてあるが、見落としがちなポイントだ。
その他のハマりポイント
| 問題 | 原因 | 解決策 |
|---|---|---|
| Appleシリコンでonnxruntimeが動かない |
onnxruntime>=1.24 がIntel Mac非対応(筆者環境にて確認) |
constraint-dependencies = ["onnxruntime<1.24"] |
| 初期同期が遅すぎる | 1件ずつembeddingしていた |
BATCH_SIZE=64 でバッチ処理 |
| MCPが他プロジェクトで使えない |
--scope user を忘れていた |
claude mcp add --scope user |
| 差分同期の状態管理 | 外部ファイルに依存したくない | QdrantのScroll APIをstateとして使う |
--scope user の件は少し恥ずかしい失敗だ。登録直後に「なんで別プロジェクトで使えないんだ」と20分悩んだ。
まとめ
MCPは「AIに自分だけの記憶を持たせる」インターフェースだと今は捉えている。
grep検索は「自分が覚えているキーワード」でしか探せない。MCPは「意味」で探せる。「楽観的ロック」と聞いて「ConditionExpression」のメモを引っ張ってくるのが意味検索だ。
初めてのMCP自作でも、既存サーバー(mcp-server-qdrant)との組み合わせで十分実用になった。自作範囲を「投入CLI」に絞れたのが大きかった。
4,000件のメモは「溜めた資産」だと思っていたが、検索できなければ「死蔵」だった。MCPを通じて初めて、過去の自分の記録を今の自分が使える形になった。grep検索には戻れない。